Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
8 changes: 8 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,14 @@ pub struct VenvArgs {
#[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)]
pub clear: bool,

/// Disable clearing and exit with error if target path is non-empty.
///
/// By default, `uv venv` will prompt to clear a non-empty directory (when a TTY is available)
/// or exit with an error (when no TTY is available). The `--no-clear` option will force
/// an error exit without prompting, regardless of TTY availability.
#[clap(long, overrides_with = "clear", conflicts_with = "allow_existing")]
pub no_clear: bool,

/// Preserve 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
Expand Down
60 changes: 41 additions & 19 deletions crates/uv-virtualenv/src/virtualenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,12 @@ pub(crate) fn create(
remove_virtualenv(&location)?;
fs::create_dir_all(&location)?;
}
OnExisting::Fail => {
let confirmation = if is_virtualenv {
confirm_clear(location, name)?
} else {
// Refuse to remove a non-virtual environment; don't even prompt.
Some(false)
};

match confirmation {
OnExisting::Fail(allow_prompt) => {
match allow_prompt
.then(|| confirm_clear(location, name))
.transpose()?
.flatten()
{
Some(true) => {
debug!("Removing existing {name} due to confirmation");
// Before removing the virtual environment, we need to canonicalize the
Expand Down Expand Up @@ -168,12 +165,30 @@ pub(crate) fn create(
)));
}
// When we don't have a TTY, warn that the behavior will change in the future
// Or when --no-clear was specified, fail without prompting
None => {
warn_user_once!(
"A {name} already exists at `{}`. In the future, uv will require `{}` to replace it",
location.user_display(),
"--clear".green(),
);
if allow_prompt {
warn_user_once!(
"A {name} already exists at `{}`. In the future, uv will require `{}` to replace it",
location.user_display(),
"--clear".green(),
);
} else {
let hint = format!(
"Use the `{}` flag to clear the {name} or `{}` to allow overwriting",
"--clear".green(),
"--allow-existing".green()
);
return Err(Error::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"A {name} already exists at {}\n\n{}{} {hint}",
location.user_display(),
"error".bold().red(),
":".bold(),
),
)));
}
}
}
}
Expand Down Expand Up @@ -637,26 +652,33 @@ pub fn remove_virtualenv(location: &Path) -> Result<(), Error> {
Ok(())
}

#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum OnExisting {
/// Fail if the directory already exists and is non-empty.
#[default]
Fail,
/// The bool parameter controls whether prompting is allowed.
Fail(bool),
/// Allow an existing directory, overwriting virtual environment files while retaining other
/// files in the directory.
Allow,
/// Remove an existing directory.
Remove,
}

impl Default for OnExisting {
fn default() -> Self {
Self::Fail(true) // Allow prompting by default
}
}

impl OnExisting {
pub fn from_args(allow_existing: bool, clear: bool) -> Self {
pub fn from_args(allow_existing: bool, clear: bool, no_clear: bool) -> Self {
if allow_existing {
Self::Allow
} else if clear {
Self::Remove
} else {
Self::default()
// If no_clear is true, don't allow prompting
Self::Fail(!no_clear)
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,11 @@ 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);
let on_existing = uv_virtualenv::OnExisting::from_args(
args.allow_existing,
args.clear,
args.no_clear,
);

commands::venv(
&project_dir,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2721,6 +2721,7 @@ pub(crate) struct VenvSettings {
pub(crate) seed: bool,
pub(crate) allow_existing: bool,
pub(crate) clear: bool,
pub(crate) no_clear: bool,
pub(crate) path: Option<PathBuf>,
pub(crate) prompt: Option<String>,
pub(crate) system_site_packages: bool,
Expand All @@ -2740,6 +2741,7 @@ impl VenvSettings {
seed,
allow_existing,
clear,
no_clear,
path,
prompt,
system_site_packages,
Expand All @@ -2759,6 +2761,7 @@ impl VenvSettings {
seed,
allow_existing,
clear,
no_clear,
path,
prompt,
system_site_packages,
Expand Down
140 changes: 128 additions & 12 deletions crates/uv/tests/it/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -967,17 +967,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: A directory already exists at: .venv

hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing directory
warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it
Activate with: source .venv/[BIN]/activate
"
);

Expand Down Expand Up @@ -1013,17 +1011,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: A directory already exists at: .venv

hint: Use the `--clear` flag or set `UV_VENV_CLEAR=1` to replace the existing directory
warning: A directory already exists at `.venv`. In the future, uv will require `--clear` to replace it
Activate with: source .venv/[BIN]/activate
Comment thread
zanieb marked this conversation as resolved.
Outdated
"
);

Expand Down Expand Up @@ -1675,3 +1671,123 @@ fn create_venv_current_working_directory() {
"
);
}

#[test]
fn no_clear_with_existing_directory() {
let context = TestContext::new_with_versions(&["3.12"]);

// Create a virtual environment first
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
Activate with: source .venv/[BIN]/activate
"###
);

// Try to create again with --no-clear (should fail)
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--no-clear")
.arg("--python")
.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
error: Failed to create virtual environment
Caused by: A virtual environment already exists at .venv

error: Use the `--clear` flag to clear the virtual environment or `--allow-existing` to allow overwriting
"
);
}

#[test]
fn no_clear_with_non_existent_directory() {
let context = TestContext::new_with_versions(&["3.12"]);

// Create with --no-clear on non-existent directory (should succeed)
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--no-clear")
.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
Activate with: source .venv/[BIN]/activate
"###
);

context.venv.assert(predicates::path::is_dir());
}

#[test]
fn no_clear_overrides_clear() {
let context = TestContext::new_with_versions(&["3.12"]);

// Create a non-empty directory at `.venv`
context.venv.create_dir_all().unwrap();
context.venv.child("file").touch().unwrap();

// --no-clear should override --clear and fail without prompting
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--clear")
.arg("--no-clear")
.arg("--python")
.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
error: Failed to create virtual environment
Caused by: A directory already exists at .venv

error: Use the `--clear` flag to clear the directory or `--allow-existing` to allow overwriting
"
);
}

#[test]
fn no_clear_conflicts_with_allow_existing() {
let context = TestContext::new_with_versions(&["3.12"]);

// Try to use --no-clear with --allow-existing (should fail)
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--no-clear")
.arg("--allow-existing")
.arg("--python")
.arg("3.12"), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: the argument '--no-clear' cannot be used with '--allow-existing'

Usage: uv venv --cache-dir [CACHE_DIR] --no-clear --python <PYTHON> --exclude-newer <EXCLUDE_NEWER> <PATH>

For more information, try '--help'.
"
);
}
4 changes: 3 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -5554,7 +5554,9 @@ uv venv [OPTIONS] [PATH]
<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>
<p>However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.</p>
<p>May also be set with the <code>UV_NATIVE_TLS</code> environment variable.</p></dd><dt id="uv-venv--no-cache"><a href="#uv-venv--no-cache"><code>--no-cache</code></a>, <code>--no-cache-dir</code>, <code>-n</code></dt><dd><p>Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation</p>
<p>May also be set with the <code>UV_NO_CACHE</code> environment variable.</p></dd><dt id="uv-venv--no-config"><a href="#uv-venv--no-config"><code>--no-config</code></a></dt><dd><p>Avoid discovering configuration files (<code>pyproject.toml</code>, <code>uv.toml</code>).</p>
<p>May also be set with the <code>UV_NO_CACHE</code> environment variable.</p></dd><dt id="uv-venv--no-clear"><a href="#uv-venv--no-clear"><code>--no-clear</code></a></dt><dd><p>Disable clearing and exit with error if target path is non-empty.</p>
<p>By default, <code>uv venv</code> will prompt to clear a non-empty directory (when a TTY is available) or exit with an error (when no TTY is available). The <code>--no-clear</code> option will force an error exit without prompting, regardless of TTY availability.</p>
</dd><dt id="uv-venv--no-config"><a href="#uv-venv--no-config"><code>--no-config</code></a></dt><dd><p>Avoid discovering configuration files (<code>pyproject.toml</code>, <code>uv.toml</code>).</p>
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p></dd><dt id="uv-venv--no-index"><a href="#uv-venv--no-index"><code>--no-index</code></a></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
</dd><dt id="uv-venv--no-managed-python"><a href="#uv-venv--no-managed-python"><code>--no-managed-python</code></a></dt><dd><p>Disable use of uv-managed Python versions.</p>
Expand Down
Loading