Consolidate logic for checking for a virtual environment (#14214)

We were checking whether a path was an executable in a virtual
environment or the base directory of a virtual environment in multiple
places in the codebase. This PR consolidates this logic into one place.

Closes #13947.
This commit is contained in:
John Mumm 2025-06-23 09:12:43 -04:00 committed by GitHub
parent a9a9e71481
commit 6481aa3e64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 30 additions and 16 deletions

View file

@ -575,6 +575,30 @@ pub fn is_temporary(path: impl AsRef<Path>) -> bool {
.is_some_and(|name| name.starts_with(".tmp"))
}
/// Checks if the grandparent directory of the given executable is the base
/// of a virtual environment.
///
/// The procedure described in PEP 405 includes checking both the parent and
/// grandparent directory of an executable, but in practice we've found this to
/// be unnecessary.
pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
executable
.as_ref()
.parent()
.and_then(Path::parent)
.is_some_and(is_virtualenv_base)
}
/// Returns `true` if a path is the base path of a virtual environment,
/// indicated by the presence of a `pyvenv.cfg` file.
///
/// The procedure described in PEP 405 includes scanning `pyvenv.cfg`
/// for a `home` key, but in practice we've found this to be
/// unnecessary.
pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
path.as_ref().join("pyvenv.cfg").is_file()
}
/// A file lock that is automatically released when dropped.
#[derive(Debug)]
pub struct LockedFile(fs_err::File);

View file

@ -888,13 +888,8 @@ impl Error {
| 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)
.then(|| {
path.parent()
.and_then(Path::parent)
.map(|path| path.join("pyvenv.cfg").try_exists())
})
.flatten()
if matches!(source, PythonSource::ActiveEnvironment)
&& uv_fs::is_virtualenv_executable(path)
{
true
} else {

View file

@ -993,14 +993,9 @@ impl InterpreterInfo {
.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,
venv: uv_fs::is_virtualenv_executable(executable),
})
} else {
Error::NotFound(executable.to_path_buf())

View file

@ -130,14 +130,14 @@ pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
for dir in current_dir.ancestors() {
// If we're _within_ a virtualenv, return it.
if dir.join("pyvenv.cfg").is_file() {
if uv_fs::is_virtualenv_base(dir) {
return Ok(Some(dir.to_path_buf()));
}
// Otherwise, search for a `.venv` directory.
let dot_venv = dir.join(".venv");
if dot_venv.is_dir() {
if !dot_venv.join("pyvenv.cfg").is_file() {
if !uv_fs::is_virtualenv_base(&dot_venv) {
return Err(Error::MissingPyVenvCfg(dot_venv));
}
return Ok(Some(dot_venv));

View file

@ -85,7 +85,7 @@ pub(crate) fn create(
} else if metadata.is_dir() {
if allow_existing {
debug!("Allowing existing directory");
} else if location.join("pyvenv.cfg").is_file() {
} else if uv_fs::is_virtualenv_base(location) {
debug!("Removing existing directory");
// On Windows, if the current executable is in the directory, guard against