diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs index 56f52b3b20..8662172632 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -121,7 +121,7 @@ impl Options { .ok() .map(PythonPath::from_virtual_env_var) }) - .unwrap_or_else(|| PythonPath::KnownSitePackages(vec![])), + .unwrap_or(PythonPath::Discover), } } diff --git a/crates/red_knot_python_semantic/src/module_resolver/resolver.rs b/crates/red_knot_python_semantic/src/module_resolver/resolver.rs index 2c6f8f3dcd..0d35cbd082 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/resolver.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/resolver.rs @@ -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 { + 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)) diff --git a/crates/red_knot_python_semantic/src/program.rs b/crates/red_knot_python_semantic/src/program.rs index 325d594faf..73c553d14d 100644 --- a/crates/red_knot_python_semantic/src/program.rs +++ b/crates/red_knot_python_semantic/src/program.rs @@ -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` diff --git a/crates/red_knot_python_semantic/src/site_packages.rs b/crates/red_knot_python_semantic/src/site_packages.rs index 6cfbd3ef8a..c4b76d877e 100644 --- a/crates/red_knot_python_semantic/src/site_packages.rs +++ b/crates/red_knot_python_semantic/src/site_packages.rs @@ -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"), } } }