Improve error message when a virtual environment Python symlink is broken (#12168)

When removing a Python interpreter underneath an existing venv, uv
currently shows a not found error:

```
error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
  Caused by: Python interpreter not found at `/home/konsti/projects/uv/.venv/bin/python3`
```

This is unintuitive, as the file for the Python interpreter does exist,
it is a broken symlink that needs to be replaced with `uv venv`.

I've been encountering those occasionally, and I expect users that
switch between versions a lot will, too, especially when they also use
pyenv or a similar Python manager.

The new error hints at this solution:

```
error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
  Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?

hint: To recreate the virtual environment, run `uv venv`
```
This commit is contained in:
konsti 2025-05-07 20:24:53 +02:00 committed by GitHub
parent a43333351e
commit 364e3999d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 134 additions and 36 deletions

View file

@ -35,7 +35,7 @@ use crate::virtualenv::{
}; };
#[cfg(windows)] #[cfg(windows)]
use crate::windows_registry::{registry_pythons, WindowsPython}; use crate::windows_registry::{registry_pythons, WindowsPython};
use crate::{Interpreter, PythonVersion}; use crate::{BrokenSymlink, Interpreter, PythonVersion};
/// A request to find a Python installation. /// A request to find a Python installation.
/// ///
@ -815,7 +815,8 @@ impl Error {
); );
false false
} }
InterpreterError::NotFound(path) => { InterpreterError::NotFound(path)
| InterpreterError::BrokenSymlink(BrokenSymlink { path, .. }) => {
// If the interpreter is from an active, valid virtual environment, we should // If the interpreter is from an active, valid virtual environment, we should
// fail because it's broken // fail because it's broken
if let Some(Ok(true)) = matches!(source, PythonSource::ActiveEnvironment) if let Some(Ok(true)) = matches!(source, PythonSource::ActiveEnvironment)
@ -894,11 +895,13 @@ pub fn find_python_installations<'a>(
debug!("Checking for Python interpreter at {request}"); debug!("Checking for Python interpreter at {request}");
match python_installation_from_executable(path, cache) { match python_installation_from_executable(path, cache) {
Ok(installation) => Ok(Ok(installation)), Ok(installation) => Ok(Ok(installation)),
Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound { Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
request: request.clone(), Ok(Err(PythonNotFound {
python_preference: preference, request: request.clone(),
environment_preference: environments, python_preference: preference,
})), environment_preference: environments,
}))
}
Err(err) => Err(Error::Query( Err(err) => Err(Error::Query(
Box::new(err), Box::new(err),
path.clone(), path.clone(),
@ -918,11 +921,13 @@ pub fn find_python_installations<'a>(
debug!("Checking for Python interpreter in {request}"); debug!("Checking for Python interpreter in {request}");
match python_installation_from_directory(path, cache) { match python_installation_from_directory(path, cache) {
Ok(installation) => Ok(Ok(installation)), Ok(installation) => Ok(Ok(installation)),
Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound { Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
request: request.clone(), Ok(Err(PythonNotFound {
python_preference: preference, request: request.clone(),
environment_preference: environments, python_preference: preference,
})), environment_preference: environments,
}))
}
Err(err) => Err(Error::Query( Err(err) => Err(Error::Query(
Box::new(err), Box::new(err),
path.clone(), path.clone(),

View file

@ -677,6 +677,8 @@ impl Display for StatusCodeError {
pub enum Error { pub enum Error {
#[error("Failed to query Python interpreter")] #[error("Failed to query Python interpreter")]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error(transparent)]
BrokenSymlink(BrokenSymlink),
#[error("Python interpreter not found at `{0}`")] #[error("Python interpreter not found at `{0}`")]
NotFound(PathBuf), NotFound(PathBuf),
#[error("Failed to query Python interpreter at `{path}`")] #[error("Failed to query Python interpreter at `{path}`")]
@ -699,6 +701,33 @@ pub enum Error {
Encode(#[from] rmp_serde::encode::Error), Encode(#[from] rmp_serde::encode::Error),
} }
#[derive(Debug, Error)]
pub struct BrokenSymlink {
pub path: PathBuf,
/// Whether the interpreter path looks like a virtual environment.
pub venv: bool,
}
impl Display for BrokenSymlink {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Broken symlink at `{}`, was the underlying Python interpreter removed?",
self.path.user_display()
)?;
if self.venv {
writeln!(
f,
"\n\n{}{} Consider recreating the environment (e.g., with `{}`)",
"hint".bold().cyan(),
":".bold(),
"uv venv".green()
)?;
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "result", rename_all = "lowercase")] #[serde(tag = "result", rename_all = "lowercase")]
enum InterpreterInfoResult { enum InterpreterInfoResult {
@ -879,7 +908,24 @@ impl InterpreterInfo {
.and_then(Timestamp::from_path) .and_then(Timestamp::from_path)
.map_err(|err| { .map_err(|err| {
if err.kind() == io::ErrorKind::NotFound { if err.kind() == io::ErrorKind::NotFound {
Error::NotFound(executable.to_path_buf()) // Check if it looks like a venv interpreter where the underlying Python
// installation was removed.
if absolute
.symlink_metadata()
.is_ok_and(|metadata| metadata.is_symlink())
{
let venv = executable
.parent()
.and_then(Path::parent)
.map(|path| path.join("pyvenv.cfg").is_file())
.unwrap_or(false);
Error::BrokenSymlink(BrokenSymlink {
path: executable.to_path_buf(),
venv,
})
} else {
Error::NotFound(executable.to_path_buf())
}
} else { } else {
err.into() err.into()
} }

View file

@ -11,7 +11,7 @@ pub use crate::discovery::{
pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment}; pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};
pub use crate::implementation::ImplementationName; pub use crate::implementation::ImplementationName;
pub use crate::installation::{PythonInstallation, PythonInstallationKey}; pub use crate::installation::{PythonInstallation, PythonInstallationKey};
pub use crate::interpreter::{Error as InterpreterError, Interpreter}; pub use crate::interpreter::{BrokenSymlink, Error as InterpreterError, Interpreter};
pub use crate::pointer_size::PointerSize; pub use crate::pointer_size::PointerSize;
pub use crate::prefix::Prefix; pub use crate::prefix::Prefix;
pub use crate::python_version::PythonVersion; pub use crate::python_version::PythonVersion;

View file

@ -227,19 +227,22 @@ impl InstalledTools {
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound( Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(
interpreter_path, interpreter_path,
))) => { ))) => {
if interpreter_path.is_symlink() { warn!(
let target_path = fs_err::read_link(&interpreter_path)?; "Ignoring existing virtual environment with missing Python interpreter: {}",
warn!( interpreter_path.user_display()
"Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}", );
interpreter_path.user_display(),
target_path.user_display() Ok(None)
); }
} else { Err(uv_python::Error::Query(uv_python::InterpreterError::BrokenSymlink(
warn!( broken_symlink,
"Ignoring existing virtual environment with missing Python interpreter: {}", ))) => {
interpreter_path.user_display() let target_path = fs_err::read_link(&broken_symlink.path)?;
); warn!(
} "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
broken_symlink.path.user_display(),
target_path.user_display()
);
Ok(None) Ok(None)
} }

View file

@ -914,15 +914,16 @@ impl ProjectInterpreter {
InvalidEnvironmentKind::Empty => {} InvalidEnvironmentKind::Empty => {}
} }
} }
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(path))) => { Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(_))) => {}
if path.is_symlink() { Err(uv_python::Error::Query(uv_python::InterpreterError::BrokenSymlink(
let target_path = fs_err::read_link(&path)?; broken_symlink,
warn_user!( ))) => {
"Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}", let target_path = fs_err::read_link(&broken_symlink.path)?;
path.user_display().cyan(), warn_user!(
target_path.user_display().cyan(), "Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
); broken_symlink.path.user_display().cyan(),
} target_path.user_display().cyan(),
);
} }
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
} }

View file

@ -17360,3 +17360,46 @@ fn incompatible_cuda() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(unix)]
#[test]
fn compile_broken_active_venv() -> Result<()> {
let context = TestContext::new("3.12");
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("anyio==3.7.0")?;
// A broken system Python
let broken_system_python = context.temp_dir.join("python3.14159");
fs_err::os::unix::fs::symlink("/does/not/exist", &broken_system_python)?;
uv_snapshot!(context
.venv()
.arg("--python")
.arg(&broken_system_python)
.arg("venv2"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for path `python3.14159` in managed installations or search path
");
// Simulate a removed Python interpreter
fs_err::remove_file(context.interpreter())?;
fs_err::os::unix::fs::symlink("/removed/python/interpreter", context.interpreter())?;
uv_snapshot!(context
.pip_compile()
.arg("requirements.in"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to inspect Python interpreter from active virtual environment at `.venv/bin/python3`
Caused by: Broken symlink at `.venv/bin/python3`, was the underlying Python interpreter removed?
hint: Consider recreating the environment (e.g., with `uv venv`)
");
Ok(())
}