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
6 changed files with 134 additions and 36 deletions
+47 -1
View File
@@ -677,6 +677,8 @@ impl Display for StatusCodeError {
pub enum Error {
#[error("Failed to query Python interpreter")]
Io(#[from] io::Error),
#[error(transparent)]
BrokenSymlink(BrokenSymlink),
#[error("Python interpreter not found at `{0}`")]
NotFound(PathBuf),
#[error("Failed to query Python interpreter at `{path}`")]
@@ -699,6 +701,33 @@ pub enum 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)]
#[serde(tag = "result", rename_all = "lowercase")]
enum InterpreterInfoResult {
@@ -879,7 +908,24 @@ impl InterpreterInfo {
.and_then(Timestamp::from_path)
.map_err(|err| {
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 {
err.into()
}