Skip to content

Commit dde8469

Browse files
jtfmummzanieb
andauthored
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 <[email protected]>
1 parent 04ee608 commit dde8469

File tree

25 files changed

+282
-135
lines changed

25 files changed

+282
-135
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,7 +1042,7 @@ jobs:
10421042
10431043
- name: "Create a virtual environment (uv)"
10441044
run: |
1045-
./uv venv -p 3.13t --managed-python
1045+
./uv venv -c -p 3.13t --managed-python
10461046
10471047
- name: "Check version (uv)"
10481048
run: |
@@ -1087,7 +1087,7 @@ jobs:
10871087
10881088
- name: "Create a virtual environment (uv)"
10891089
run: |
1090-
./uv venv -p 3.13 --managed-python
1090+
./uv venv -c -p 3.13 --managed-python
10911091
10921092
- name: "Check version (uv)"
10931093
run: |
@@ -1132,7 +1132,7 @@ jobs:
11321132
11331133
- name: "Create a virtual environment (uv)"
11341134
run: |
1135-
./uv venv -p 3.13 --managed-python
1135+
./uv venv -c -p 3.13 --managed-python
11361136
11371137
- name: "Check version (uv)"
11381138
run: |
@@ -1758,14 +1758,14 @@ jobs:
17581758
./uv run --no-project python -c "from built_by_uv import greet; print(greet())"
17591759
17601760
# Test both `build_wheel` and `build_sdist` through uv
1761-
./uv venv -v
1761+
./uv venv -c -v
17621762
./uv build -v --force-pep517 scripts/packages/built-by-uv --find-links crates/uv-build/dist --offline
17631763
./uv pip install -v scripts/packages/built-by-uv/dist/*.tar.gz --find-links crates/uv-build/dist --offline --no-deps
17641764
./uv run --no-project python -c "from built_by_uv import greet; print(greet())"
17651765
17661766
# Test both `build_wheel` and `build_sdist` through the official `build`
17671767
rm -rf scripts/packages/built-by-uv/dist/
1768-
./uv venv -v
1768+
./uv venv -c -v
17691769
./uv pip install build
17701770
# Add the uv binary to PATH for `build` to find
17711771
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

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-build-frontend/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ impl SourceBuild {
331331
interpreter.clone(),
332332
uv_virtualenv::Prompt::None,
333333
false,
334-
false,
334+
uv_virtualenv::OnExisting::Remove,
335335
false,
336336
false,
337337
false,

crates/uv-cli/src/compat.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,6 @@ enum Resolver {
266266
/// These represent a subset of the `virtualenv` interface that uv supports by default.
267267
#[derive(Args)]
268268
pub struct VenvCompatArgs {
269-
#[clap(long, hide = true)]
270-
clear: bool,
271-
272269
#[clap(long, hide = true)]
273270
no_seed: bool,
274271

@@ -289,12 +286,6 @@ impl CompatArgs for VenvCompatArgs {
289286
/// behavior. If an argument is passed that does _not_ match uv's behavior, this method will
290287
/// return an error.
291288
fn validate(&self) -> Result<()> {
292-
if self.clear {
293-
warn_user!(
294-
"virtualenv's `--clear` has no effect (uv always clears the virtual environment)"
295-
);
296-
}
297-
298289
if self.no_seed {
299290
warn_user!(
300291
"virtualenv's `--no-seed` has no effect (uv omits seed packages by default)"

crates/uv-cli/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,16 +2615,23 @@ pub struct VenvArgs {
26152615
#[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_SEED)]
26162616
pub seed: bool,
26172617

2618+
/// Remove any existing files or directories at the target path.
2619+
///
2620+
/// By default, `uv venv` will exit with an error if the given path is non-empty. The
2621+
/// `--clear` option will instead clear a non-empty path before creating a new virtual
2622+
/// environment.
2623+
#[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)]
2624+
pub clear: bool,
2625+
26182626
/// Preserve any existing files or directories at the target path.
26192627
///
2620-
/// By default, `uv venv` will remove an existing virtual environment at the given path, and
2621-
/// exit with an error if the path is non-empty but _not_ a virtual environment. The
2628+
/// By default, `uv venv` will exit with an error if the given path is non-empty. The
26222629
/// `--allow-existing` option will instead write to the given path, regardless of its contents,
26232630
/// and without clearing it beforehand.
26242631
///
26252632
/// WARNING: This option can lead to unexpected behavior if the existing virtual environment and
26262633
/// the newly-created virtual environment are linked to different Python interpreters.
2627-
#[clap(long)]
2634+
#[clap(long, overrides_with = "clear")]
26282635
pub allow_existing: bool,
26292636

26302637
/// The path to the virtual environment to create.

crates/uv-console/src/lib.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ use std::{cmp::Ordering, iter};
66
/// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report
77
/// enabled.
88
pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<bool> {
9+
confirm_inner(message, None, term, default)
10+
}
11+
12+
/// Prompt the user for confirmation in the given [`Term`], with a hint.
13+
pub fn confirm_with_hint(
14+
message: &str,
15+
hint: &str,
16+
term: &Term,
17+
default: bool,
18+
) -> std::io::Result<bool> {
19+
confirm_inner(message, Some(hint), term, default)
20+
}
21+
22+
fn confirm_inner(
23+
message: &str,
24+
hint: Option<&str>,
25+
term: &Term,
26+
default: bool,
27+
) -> std::io::Result<bool> {
928
let prompt = format!(
1029
"{} {} {} {} {}",
1130
style("?".to_string()).for_stderr().yellow(),
@@ -18,6 +37,13 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<boo
1837
);
1938

2039
term.write_str(&prompt)?;
40+
if let Some(hint) = hint {
41+
term.write_str(&format!(
42+
"\n\n{}{} {hint}",
43+
style("hint").for_stderr().bold().cyan(),
44+
style(":").for_stderr().bold()
45+
))?;
46+
}
2147
term.hide_cursor()?;
2248
term.flush()?;
2349

@@ -56,7 +82,11 @@ pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<boo
5682
.cyan(),
5783
);
5884

59-
term.clear_line()?;
85+
if hint.is_some() {
86+
term.clear_last_lines(2)?;
87+
} else {
88+
term.clear_line()?;
89+
}
6090
term.write_line(&report)?;
6191
term.show_cursor()?;
6292
term.flush()?;

crates/uv-static/src/env_vars.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ impl EnvVars {
292292
/// Distributions can be read from a local directory by using the `file://` URL scheme.
293293
pub const UV_PYPY_INSTALL_MIRROR: &'static str = "UV_PYPY_INSTALL_MIRROR";
294294

295+
/// Equivalent to the `--clear` command-line argument. If set, uv will remove any
296+
/// existing files or directories at the target path.
297+
pub const UV_VENV_CLEAR: &'static str = "UV_VENV_CLEAR";
298+
295299
/// Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual environment
296300
/// created by `uv venv`.
297301
///

crates/uv-tool/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ impl InstalledTools {
285285
interpreter,
286286
uv_virtualenv::Prompt::None,
287287
false,
288-
false,
288+
uv_virtualenv::OnExisting::Remove,
289289
false,
290290
false,
291291
false,

crates/uv-virtualenv/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@ workspace = true
2121

2222
[dependencies]
2323
uv-configuration = { workspace = true }
24+
uv-console = { workspace = true }
2425
uv-fs = { workspace = true }
2526
uv-pypi-types = { workspace = true }
2627
uv-python = { workspace = true }
2728
uv-shell = { workspace = true }
2829
uv-version = { workspace = true }
30+
uv-warnings = { workspace = true }
2931

32+
console = { workspace = true }
3033
fs-err = { workspace = true }
3134
itertools = { workspace = true }
35+
owo-colors = { workspace = true }
3236
pathdiff = { workspace = true }
3337
thiserror = { workspace = true }
3438
tracing = { workspace = true }

crates/uv-virtualenv/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use thiserror::Error;
66
use uv_configuration::PreviewMode;
77
use uv_python::{Interpreter, PythonEnvironment};
88

9+
pub use virtualenv::OnExisting;
10+
911
mod virtualenv;
1012

1113
#[derive(Debug, Error)]
@@ -50,7 +52,7 @@ pub fn create_venv(
5052
interpreter: Interpreter,
5153
prompt: Prompt,
5254
system_site_packages: bool,
53-
allow_existing: bool,
55+
on_existing: OnExisting,
5456
relocatable: bool,
5557
seed: bool,
5658
upgradeable: bool,
@@ -62,7 +64,7 @@ pub fn create_venv(
6264
&interpreter,
6365
prompt,
6466
system_site_packages,
65-
allow_existing,
67+
on_existing,
6668
relocatable,
6769
seed,
6870
upgradeable,

0 commit comments

Comments
 (0)