mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-24 13:43:45 +00:00
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:
parent
a43333351e
commit
364e3999d4
6 changed files with 134 additions and 36 deletions
|
@ -35,7 +35,7 @@ use crate::virtualenv::{
|
|||
};
|
||||
#[cfg(windows)]
|
||||
use crate::windows_registry::{registry_pythons, WindowsPython};
|
||||
use crate::{Interpreter, PythonVersion};
|
||||
use crate::{BrokenSymlink, Interpreter, PythonVersion};
|
||||
|
||||
/// A request to find a Python installation.
|
||||
///
|
||||
|
@ -815,7 +815,8 @@ impl Error {
|
|||
);
|
||||
false
|
||||
}
|
||||
InterpreterError::NotFound(path) => {
|
||||
InterpreterError::NotFound(path)
|
||||
| InterpreterError::BrokenSymlink(BrokenSymlink { path, .. }) => {
|
||||
// If the interpreter is from an active, valid virtual environment, we should
|
||||
// fail because it's broken
|
||||
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}");
|
||||
match python_installation_from_executable(path, cache) {
|
||||
Ok(installation) => Ok(Ok(installation)),
|
||||
Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound {
|
||||
Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
|
||||
Ok(Err(PythonNotFound {
|
||||
request: request.clone(),
|
||||
python_preference: preference,
|
||||
environment_preference: environments,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
Err(err) => Err(Error::Query(
|
||||
Box::new(err),
|
||||
path.clone(),
|
||||
|
@ -918,11 +921,13 @@ pub fn find_python_installations<'a>(
|
|||
debug!("Checking for Python interpreter in {request}");
|
||||
match python_installation_from_directory(path, cache) {
|
||||
Ok(installation) => Ok(Ok(installation)),
|
||||
Err(InterpreterError::NotFound(_)) => Ok(Err(PythonNotFound {
|
||||
Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
|
||||
Ok(Err(PythonNotFound {
|
||||
request: request.clone(),
|
||||
python_preference: preference,
|
||||
environment_preference: environments,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
Err(err) => Err(Error::Query(
|
||||
Box::new(err),
|
||||
path.clone(),
|
||||
|
|
|
@ -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 {
|
||||
// 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()
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ pub use crate::discovery::{
|
|||
pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};
|
||||
pub use crate::implementation::ImplementationName;
|
||||
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::prefix::Prefix;
|
||||
pub use crate::python_version::PythonVersion;
|
||||
|
|
|
@ -227,19 +227,22 @@ impl InstalledTools {
|
|||
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(
|
||||
interpreter_path,
|
||||
))) => {
|
||||
if interpreter_path.is_symlink() {
|
||||
let target_path = fs_err::read_link(&interpreter_path)?;
|
||||
warn!(
|
||||
"Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
|
||||
interpreter_path.user_display(),
|
||||
target_path.user_display()
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
"Ignoring existing virtual environment with missing Python interpreter: {}",
|
||||
interpreter_path.user_display()
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
Err(uv_python::Error::Query(uv_python::InterpreterError::BrokenSymlink(
|
||||
broken_symlink,
|
||||
))) => {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -914,16 +914,17 @@ impl ProjectInterpreter {
|
|||
InvalidEnvironmentKind::Empty => {}
|
||||
}
|
||||
}
|
||||
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(path))) => {
|
||||
if path.is_symlink() {
|
||||
let target_path = fs_err::read_link(&path)?;
|
||||
Err(uv_python::Error::Query(uv_python::InterpreterError::NotFound(_))) => {}
|
||||
Err(uv_python::Error::Query(uv_python::InterpreterError::BrokenSymlink(
|
||||
broken_symlink,
|
||||
))) => {
|
||||
let target_path = fs_err::read_link(&broken_symlink.path)?;
|
||||
warn_user!(
|
||||
"Ignoring existing virtual environment linked to non-existent Python interpreter: {} -> {}",
|
||||
path.user_display().cyan(),
|
||||
broken_symlink.path.user_display().cyan(),
|
||||
target_path.user_display().cyan(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
|
|
|
@ -17360,3 +17360,46 @@ fn incompatible_cuda() -> Result<()> {
|
|||
|
||||
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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue