From 21aa9bc53a506ed3d9fc6a3bf9a5ab1d436fd757 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 25 Nov 2024 16:23:49 -0600 Subject: [PATCH] Allow syncing to empty virtual environment directories (#9427) As discussed in https://github.com/astral-sh/uv/issues/9423, it's confusing that we do not allow `uv sync` just because the `.venv` directory _exists_. This change matches `uv venv`. --- crates/uv-python/src/environment.rs | 10 ++++++++++ crates/uv/src/commands/project/mod.rs | 15 +++++++++++---- crates/uv/tests/it/sync.rs | 10 +++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index 1dff7d3dc..d02c2fe35 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -43,6 +43,7 @@ pub struct InvalidEnvironment { #[derive(Debug, Clone)] pub enum InvalidEnvironmentKind { NotDirectory, + Empty, MissingExecutable(PathBuf), } @@ -128,6 +129,7 @@ impl std::fmt::Display for InvalidEnvironmentKind { Self::MissingExecutable(path) => { write!(f, "missing Python executable at `{}`", path.user_display()) } + Self::Empty => write!(f, "directory is empty"), } } } @@ -178,6 +180,14 @@ impl PythonEnvironment { .into()); } + if venv.read_dir().is_ok_and(|mut dir| dir.next().is_none()) { + return Err(InvalidEnvironment { + path: venv, + kind: InvalidEnvironmentKind::Empty, + } + .into()); + } + let executable = virtualenv_python_executable(&venv); // Check if the executable exists before querying so we can provide a more specific error diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 054035f14..3d12fba41 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -689,6 +689,8 @@ impl ProjectInterpreter { )); } } + // If the environment is an empty directory, it's fine to use + InvalidEnvironmentKind::Empty => {} }; } Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(path))) => { @@ -807,10 +809,15 @@ pub(crate) async fn get_or_init_environment( (Ok(false), Ok(false)) => false, // If it's not a virtual environment, bail (Ok(true), Ok(false)) => { - return Err(ProjectError::InvalidProjectEnvironmentDir( - venv, - "it is not a compatible environment but cannot be recreated because it is not a virtual environment".to_string(), - )); + // Unless it's empty, in which case we just ignore it + if venv.read_dir().is_ok_and(|mut dir| dir.next().is_none()) { + false + } else { + return Err(ProjectError::InvalidProjectEnvironmentDir( + venv, + "it is not a compatible environment but cannot be recreated because it is not a virtual environment".to_string(), + )); + } } // Similarly, if we can't _tell_ if it exists we should bail (_, Err(err)) | (Err(err), _) => { diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 3c6014912..d9537c773 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -2795,13 +2795,17 @@ fn sync_empty_virtual_environment() -> Result<()> { // Running `uv sync` should work uv_snapshot!(context.filters(), context.sync(), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] - error: Project virtual environment directory `[VENV]/` cannot be used because it is not a compatible environment but cannot be recreated because it is not a virtual environment + Creating virtual environment at: .venv + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 "###); Ok(())