[red-knot] Discover local venv folder in cli (#16917)

<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Fixes #16744 

Code from 

bbf4f830b5/crates/uv-python/src/virtualenv.rs (L124-L144)

## Test Plan

Manual testing

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Matthew Mckee 2025-03-28 17:59:49 +00:00 committed by GitHub
parent aca6254e82
commit 0e48940ea4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 53 additions and 2 deletions

View file

@ -121,7 +121,7 @@ impl Options {
.ok()
.map(PythonPath::from_virtual_env_var)
})
.unwrap_or_else(|| PythonPath::KnownSitePackages(vec![])),
.unwrap_or(PythonPath::Discover),
}
}

View file

@ -11,7 +11,7 @@ use ruff_python_ast::PythonVersion;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
use crate::site_packages::VirtualEnvironment;
use crate::site_packages::{SitePackagesDiscoveryError, SysPrefixPathOrigin, VirtualEnvironment};
use crate::{Program, PythonPath, SearchPathSettings};
use super::module::{Module, ModuleKind};
@ -133,6 +133,22 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
Program::get(db).search_paths(db).iter(db)
}
/// Searches for a `.venv` directory in the current or any parent directory
fn virtual_env_from_working_dir() -> Option<SystemPathBuf> {
let current_dir = std::env::current_dir().ok()?;
for dir in current_dir.ancestors() {
let dot_venv = dir.join(".venv");
if dot_venv.is_dir() {
if !dot_venv.join("pyvenv.cfg").is_file() {
return None;
}
return SystemPathBuf::from_path_buf(dot_venv).ok();
}
}
None
}
#[derive(Debug, PartialEq, Eq)]
pub struct SearchPaths {
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
@ -235,6 +251,37 @@ impl SearchPaths {
.and_then(|venv| venv.site_packages_directories(system))?
}
PythonPath::Discover => {
tracing::debug!("Discovering virtual environment");
let virtual_env_path = virtual_env_from_working_dir();
if let Some(virtual_env_path) = virtual_env_path {
tracing::debug!("Found `.venv` folder at '{}'", virtual_env_path);
let handle_invalid_virtual_env = |error: SitePackagesDiscoveryError| {
tracing::debug!(
"Ignoring automatically detected virtual environment at '{}': {}",
virtual_env_path,
error
);
vec![]
};
match VirtualEnvironment::new(
virtual_env_path.clone(),
SysPrefixPathOrigin::LocalVenv,
system,
) {
Ok(venv) => venv
.site_packages_directories(system)
.unwrap_or_else(handle_invalid_virtual_env),
Err(error) => handle_invalid_virtual_env(error),
}
} else {
tracing::debug!("No virtual environment found");
vec![]
}
}
PythonPath::KnownSitePackages(paths) => paths
.iter()
.map(|path| canonicalize(path, system))

View file

@ -145,6 +145,8 @@ pub enum PythonPath {
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
SysPrefix(SystemPathBuf, SysPrefixPathOrigin),
Discover,
/// Resolved site packages paths.
///
/// This variant is mainly intended for testing where we want to skip resolving `site-packages`

View file

@ -459,6 +459,7 @@ pub enum SysPrefixPathOrigin {
PythonCliFlag,
VirtualEnvVar,
Derived,
LocalVenv,
}
impl Display for SysPrefixPathOrigin {
@ -467,6 +468,7 @@ impl Display for SysPrefixPathOrigin {
Self::PythonCliFlag => f.write_str("`--python` argument"),
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
Self::Derived => f.write_str("derived `sys.prefix` path"),
Self::LocalVenv => f.write_str("local virtual environment"),
}
}
}