diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs index c44b3e5634..485aeb511b 100644 --- a/crates/ruff/tests/analyze_graph.rs +++ b/crates/ruff/tests/analyze_graph.rs @@ -566,8 +566,6 @@ fn venv() -> Result<()> { ----- stderr ----- ruff failed - Cause: Invalid search path settings - Cause: Failed to discover the site-packages directory Cause: Invalid `--python` argument `none`: does not point to a Python executable or a directory on disk Cause: No such file or directory (os error 2) "); diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index 544acf85c8..353ec737e0 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -9,7 +9,7 @@ use ruff_db::{Db as SourceDb, Upcast}; use ruff_python_ast::PythonVersion; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{ - Db, Program, ProgramSettings, PythonEnvironmentPath, PythonPlatform, PythonVersionSource, + Db, Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin, default_lint_registry, }; @@ -35,13 +35,17 @@ impl ModuleDb { python_version: PythonVersion, venv_path: Option, ) -> Result { - let mut search_paths = SearchPathSettings::new(src_roots); - // TODO: Consider setting `PythonPath::Auto` if no venv_path is provided. - if let Some(venv_path) = venv_path { - search_paths.python_environment = - PythonEnvironmentPath::explicit(venv_path, SysPrefixPathOrigin::PythonCliFlag); - } let db = Self::default(); + let mut search_paths = SearchPathSettings::new(src_roots); + // TODO: Consider calling `PythonEnvironment::discover` if the `venv_path` is not provided. + if let Some(venv_path) = venv_path { + let environment = + PythonEnvironment::new(venv_path, SysPrefixPathOrigin::PythonCliFlag, db.system())?; + search_paths.site_packages_paths = environment + .site_packages_paths(db.system()) + .context("Failed to discover the site-packages directory")? + .into_vec(); + } let search_paths = search_paths .to_search_paths(db.system(), db.vendored()) .context("Invalid search path settings")?; diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index eab7ba743b..ed74463428 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -590,7 +590,6 @@ fn python_cli_argument_virtual_environment() -> anyhow::Result<()> { ----- stderr ----- WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. ty failed - Cause: Failed to discover the site-packages directory Cause: Invalid `--python` argument `/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk "); @@ -603,7 +602,6 @@ fn python_cli_argument_virtual_environment() -> anyhow::Result<()> { ----- stderr ----- WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. ty failed - Cause: Failed to discover the site-packages directory Cause: Invalid `--python` argument `/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk Cause: No such file or directory (os error 2) "); @@ -686,7 +684,6 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> { ----- stderr ----- WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. ty failed - Cause: Failed to discover the site-packages directory Cause: Invalid `environment.python` setting --> Invalid setting in configuration file `/pyproject.toml` diff --git a/crates/ty_project/src/combine.rs b/crates/ty_project/src/combine.rs index 482b299d74..98222d4ee8 100644 --- a/crates/ty_project/src/combine.rs +++ b/crates/ty_project/src/combine.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, hash::BuildHasher}; use ordermap::OrderMap; use ruff_db::system::SystemPathBuf; use ruff_python_ast::PythonVersion; -use ty_python_semantic::{PythonEnvironmentPath, PythonPlatform}; +use ty_python_semantic::PythonPlatform; /// Combine two values, preferring the values in `self`. /// @@ -141,7 +141,6 @@ macro_rules! impl_noop_combine { impl_noop_combine!(SystemPathBuf); impl_noop_combine!(PythonPlatform); -impl_noop_combine!(PythonEnvironmentPath); impl_noop_combine!(PythonVersion); // std types diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 73e8da207c..7e2a3cd717 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -7,6 +7,7 @@ use super::settings::{Override, Settings, TerminalSettings}; use crate::metadata::value::{ RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource, ValueSourceGuard, }; +use anyhow::Context; use ordermap::OrderMap; use ruff_db::RustDoc; use ruff_db::diagnostic::{ @@ -29,9 +30,9 @@ use std::sync::Arc; use thiserror::Error; use ty_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection}; use ty_python_semantic::{ - ProgramSettings, PythonEnvironmentPath, PythonPlatform, PythonVersionFileSource, + ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError, - SearchPaths, SysPrefixPathOrigin, + SearchPaths, SitePackagesPaths, SysPrefixPathOrigin, }; #[derive( @@ -133,16 +134,52 @@ impl Options { default }); - let search_paths = self.to_search_paths(project_root, project_name, system, vendored)?; + let python_environment = if let Some(python_path) = environment.python.as_ref() { + let origin = match python_path.source() { + ValueSource::Cli => SysPrefixPathOrigin::PythonCliFlag, + ValueSource::File(path) => { + SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range()) + } + }; + + Some(PythonEnvironment::new( + python_path.absolute(project_root, system), + origin, + system, + )?) + } else { + PythonEnvironment::discover(project_root, system) + .context("Failed to discover local Python environment")? + }; + + let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() { + python_environment + .site_packages_paths(system) + .context("Failed to discover the site-packages directory")? + } else { + tracing::debug!("No virtual environment found"); + + SitePackagesPaths::default() + }; let python_version = options_python_version .or_else(|| { - search_paths - .try_resolve_installation_python_version() - .map(Cow::into_owned) + python_environment + .as_ref()? + .python_version_from_metadata() + .cloned() }) + .or_else(|| site_packages_paths.python_version_from_layout()) .unwrap_or_default(); + let search_paths = self.to_search_paths( + project_root, + project_name, + site_packages_paths, + system, + vendored, + )?; + tracing::info!( "Python version: Python {python_version}, platform: {python_platform}", python_version = python_version.version @@ -159,6 +196,7 @@ impl Options { &self, project_root: &SystemPath, project_name: &str, + site_packages_paths: SitePackagesPaths, system: &dyn System, vendored: &VendoredFileSystem, ) -> Result { @@ -230,23 +268,7 @@ impl Options { .typeshed .as_ref() .map(|path| path.absolute(project_root, system)), - python_environment: environment - .python - .as_ref() - .map(|python_path| { - let origin = match python_path.source() { - ValueSource::Cli => SysPrefixPathOrigin::PythonCliFlag, - ValueSource::File(path) => SysPrefixPathOrigin::ConfigFileSetting( - path.clone(), - python_path.range(), - ), - }; - PythonEnvironmentPath::explicit( - python_path.absolute(project_root, system), - origin, - ) - }) - .unwrap_or_else(|| PythonEnvironmentPath::Discover(project_root.to_path_buf())), + site_packages_paths: site_packages_paths.into_vec(), }; settings.to_search_paths(system, vendored) diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index d8c51e7327..86574891bf 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -11,12 +11,12 @@ pub use module_resolver::{ system_module_search_paths, }; pub use program::{ - Program, ProgramSettings, PythonEnvironmentPath, PythonVersionFileSource, PythonVersionSource, + Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings, }; pub use python_platform::PythonPlatform; pub use semantic_model::{HasType, SemanticModel}; -pub use site_packages::SysPrefixPathOrigin; +pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic; pub mod ast_node_ref; diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index 8a0206b15b..b0c7dc2349 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -1,9 +1,8 @@ use std::borrow::Cow; use std::fmt; use std::iter::FusedIterator; -use std::str::{FromStr, Split}; +use std::str::Split; -use camino::Utf8Component; use compact_str::format_compact; use rustc_hash::{FxBuildHasher, FxHashSet}; @@ -15,13 +14,7 @@ use ruff_python_ast::PythonVersion; use crate::db::Db; use crate::module_name::ModuleName; use crate::module_resolver::typeshed::{TypeshedVersions, vendored_typeshed_versions}; -use crate::site_packages::{ - PythonEnvironment, SitePackagesDiscoveryError, SitePackagesPaths, SysPrefixPathOrigin, -}; -use crate::{ - Program, PythonEnvironmentPath, PythonVersionSource, PythonVersionWithSource, - SearchPathSettings, -}; +use crate::{Program, SearchPathSettings}; use super::module::{Module, ModuleKind}; use super::path::{ModulePath, SearchPath, SearchPathValidationError}; @@ -160,13 +153,6 @@ pub struct SearchPaths { site_packages: Vec, typeshed_versions: TypeshedVersions, - - /// The Python version implied by the virtual environment. - /// - /// If this environment was a system installation or the `pyvenv.cfg` file - /// of the virtual environment did not contain a `version` or `version_info` key, - /// this field will be `None`. - python_version_from_pyvenv_cfg: Option, } impl SearchPaths { @@ -191,7 +177,7 @@ impl SearchPaths { extra_paths, src_roots, custom_typeshed: typeshed, - python_environment: python_path, + site_packages_paths, } = settings; let mut static_paths = vec![]; @@ -236,30 +222,11 @@ impl SearchPaths { static_paths.push(stdlib_path); - let (site_packages_paths, python_version) = match python_path { - PythonEnvironmentPath::Discover(project_root) => { - Self::discover_python_environment(system, project_root)? - } - - PythonEnvironmentPath::Explicit(prefix, origin) => { - tracing::debug!("Resolving {origin}: {prefix}"); - PythonEnvironment::new(prefix, origin.clone(), system)?.into_settings(system)? - } - - PythonEnvironmentPath::Testing(paths) => ( - paths - .iter() - .map(|path| canonicalize(path, system)) - .collect(), - None, - ), - }; - let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len()); for path in site_packages_paths { tracing::debug!("Adding site-packages search path '{path}'"); - site_packages.push(SearchPath::site_packages(system, path)?); + site_packages.push(SearchPath::site_packages(system, path.clone())?); } // TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step @@ -285,68 +252,9 @@ impl SearchPaths { static_paths, site_packages, typeshed_versions, - python_version_from_pyvenv_cfg: python_version, }) } - fn discover_python_environment( - system: &dyn System, - project_root: &SystemPath, - ) -> Result<(SitePackagesPaths, Option), SitePackagesDiscoveryError> - { - fn resolve_environment( - system: &dyn System, - path: &SystemPath, - origin: SysPrefixPathOrigin, - ) -> Result<(SitePackagesPaths, Option), SitePackagesDiscoveryError> - { - tracing::debug!("Resolving {origin}: {path}"); - PythonEnvironment::new(path, origin, system)?.into_settings(system) - } - - if let Ok(virtual_env) = system.env_var("VIRTUAL_ENV") { - return resolve_environment( - system, - SystemPath::new(&virtual_env), - SysPrefixPathOrigin::VirtualEnvVar, - ); - } - - if let Ok(conda_env) = system.env_var("CONDA_PREFIX") { - return resolve_environment( - system, - SystemPath::new(&conda_env), - SysPrefixPathOrigin::CondaPrefixVar, - ); - } - - tracing::debug!("Discovering virtual environment in `{project_root}`"); - let virtual_env_directory = project_root.join(".venv"); - - match PythonEnvironment::new( - &virtual_env_directory, - SysPrefixPathOrigin::LocalVenv, - system, - ) - .and_then(|venv| venv.into_settings(system)) - { - Ok(settings) => return Ok(settings), - Err(err) => { - if system.is_directory(&virtual_env_directory) { - tracing::debug!( - "Ignoring automatically detected virtual environment at `{}`: {}", - &virtual_env_directory, - err - ); - } - } - } - - tracing::debug!("No virtual environment found"); - - Ok((SitePackagesPaths::default(), None)) - } - pub(crate) fn try_register_static_roots(&self, db: &dyn Db) { let files = db.files(); for path in self.static_paths.iter().chain(self.site_packages.iter()) { @@ -379,52 +287,6 @@ impl SearchPaths { pub(crate) fn typeshed_versions(&self) -> &TypeshedVersions { &self.typeshed_versions } - - pub fn try_resolve_installation_python_version(&self) -> Option> { - if let Some(version) = self.python_version_from_pyvenv_cfg.as_ref() { - return Some(Cow::Borrowed(version)); - } - - if cfg!(windows) { - // The path to `site-packages` on Unix is - // `/lib/pythonX.Y/site-packages`, - // but on Windows it's `/Lib/site-packages`. - return None; - } - - let primary_site_packages = self.site_packages.first()?.as_system_path()?; - - let mut site_packages_ancestor_components = - primary_site_packages.components().rev().skip(1).map(|c| { - // This should have all been validated in `site_packages.rs` - // when we resolved the search paths for the project. - debug_assert!( - matches!(c, Utf8Component::Normal(_)), - "Unexpected component in site-packages path `{c:?}` \ - (expected `site-packages` to be an absolute path with symlinks resolved, \ - located at `/lib/pythonX.Y/site-packages`)" - ); - - c.as_str() - }); - - let parent_component = site_packages_ancestor_components.next()?; - - if site_packages_ancestor_components.next()? != "lib" { - return None; - } - - let version = parent_component - .strip_prefix("python") - .or_else(|| parent_component.strip_prefix("pypy"))? - .trim_end_matches('t'); - - let version = PythonVersion::from_str(version).ok()?; - let source = PythonVersionSource::InstallationDirectoryLayout { - site_packages_parent_dir: Box::from(parent_component), - }; - Some(Cow::Owned(PythonVersionWithSource { version, source })) - } } /// Collect all dynamic search paths. For each `site-packages` path: @@ -443,7 +305,6 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec { static_paths, site_packages, typeshed_versions: _, - python_version_from_pyvenv_cfg: _, } = Program::get(db).search_paths(db); let mut dynamic_paths = Vec::new(); @@ -1534,7 +1395,7 @@ mod tests { python_platform: PythonPlatform::default(), search_paths: SearchPathSettings { custom_typeshed: Some(custom_typeshed), - python_environment: PythonEnvironmentPath::Testing(vec![site_packages]), + site_packages_paths: vec![site_packages], ..SearchPathSettings::new(vec![src.clone()]) } .to_search_paths(db.system(), db.vendored()) @@ -2049,10 +1910,7 @@ not_a_directory python_version: PythonVersionWithSource::default(), python_platform: PythonPlatform::default(), search_paths: SearchPathSettings { - python_environment: PythonEnvironmentPath::Testing(vec![ - venv_site_packages, - system_site_packages, - ]), + site_packages_paths: vec![venv_site_packages, system_site_packages], ..SearchPathSettings::new(vec![SystemPathBuf::from("/src")]) } .to_search_paths(db.system(), db.vendored()) @@ -2166,7 +2024,7 @@ not_a_directory python_version: PythonVersionWithSource::default(), python_platform: PythonPlatform::default(), search_paths: SearchPathSettings { - python_environment: PythonEnvironmentPath::Testing(vec![site_packages.clone()]), + site_packages_paths: vec![site_packages.clone()], ..SearchPathSettings::new(vec![project_directory]) } .to_search_paths(db.system(), db.vendored()) diff --git a/crates/ty_python_semantic/src/module_resolver/testing.rs b/crates/ty_python_semantic/src/module_resolver/testing.rs index 3367bd1270..9b06764e16 100644 --- a/crates/ty_python_semantic/src/module_resolver/testing.rs +++ b/crates/ty_python_semantic/src/module_resolver/testing.rs @@ -7,10 +7,7 @@ use ruff_python_ast::PythonVersion; use crate::db::tests::TestDb; use crate::program::{Program, SearchPathSettings}; -use crate::{ - ProgramSettings, PythonEnvironmentPath, PythonPlatform, PythonVersionSource, - PythonVersionWithSource, -}; +use crate::{ProgramSettings, PythonPlatform, PythonVersionSource, PythonVersionWithSource}; /// A test case for the module resolver. /// @@ -246,7 +243,7 @@ impl TestCaseBuilder { python_platform, search_paths: SearchPathSettings { custom_typeshed: Some(typeshed.clone()), - python_environment: PythonEnvironmentPath::Testing(vec![site_packages.clone()]), + site_packages_paths: vec![site_packages.clone()], ..SearchPathSettings::new(vec![src.clone()]) } .to_search_paths(db.system(), db.vendored()) @@ -306,7 +303,7 @@ impl TestCaseBuilder { }, python_platform, search_paths: SearchPathSettings { - python_environment: PythonEnvironmentPath::Testing(vec![site_packages.clone()]), + site_packages_paths: vec![site_packages.clone()], ..SearchPathSettings::new(vec![src.clone()]) } .to_search_paths(db.system(), db.vendored()) diff --git a/crates/ty_python_semantic/src/program.rs b/crates/ty_python_semantic/src/program.rs index 5e642de0c4..4d09b17712 100644 --- a/crates/ty_python_semantic/src/program.rs +++ b/crates/ty_python_semantic/src/program.rs @@ -1,8 +1,8 @@ use std::sync::Arc; +use crate::Db; use crate::module_resolver::{SearchPathValidationError, SearchPaths}; use crate::python_platform::PythonPlatform; -use crate::{Db, SysPrefixPathOrigin}; use ruff_db::diagnostic::Span; use ruff_db::files::system_path_to_file; @@ -173,9 +173,8 @@ pub struct SearchPathSettings { /// bundled as a zip file in the binary pub custom_typeshed: Option, - /// Path to the Python environment from which ty resolves third party dependencies - /// and their type information. - pub python_environment: PythonEnvironmentPath, + /// List of site packages paths to use. + pub site_packages_paths: Vec, } impl SearchPathSettings { @@ -191,7 +190,7 @@ impl SearchPathSettings { src_roots: vec![], extra_paths: vec![], custom_typeshed: None, - python_environment: PythonEnvironmentPath::Testing(vec![]), + site_packages_paths: vec![], } } @@ -203,43 +202,3 @@ impl SearchPathSettings { SearchPaths::from_settings(self, system, vendored) } } - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum PythonEnvironmentPath { - /// The path to the Python environment isn't known. Try to discover the Python environment - /// by inspecting environment variables, the project structure, etc. and derive the path from it. - /// - /// The path is the project root in which to search for a Python environment. - Discover(SystemPathBuf), - - /// Path to a Python environment that is explicitly specified. - /// - /// The path that either represents the value of [`sys.prefix`] at runtime in Python - /// for a given Python executable, or which represents a path relative to `sys.prefix` - /// that we will attempt later to resolve into `sys.prefix`. Exactly which this variant - /// represents depends on the [`SysPrefixPathOrigin`] element in the tuple. - /// - /// For the case of a virtual environment, where a - /// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to - /// the virtual environment the Python binary lies inside, i.e. `/.venv`, - /// and `site-packages` will be at `.venv/lib/python3.X/site-packages`. - /// System Python installations generally work the same way: if a system - /// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix` - /// will be `/opt/homebrew`, and `site-packages` will be at - /// `/opt/homebrew/lib/python3.X/site-packages`. - /// - /// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix - Explicit(SystemPathBuf, SysPrefixPathOrigin), - - /// Don't search for a Python environment, instead use the provided site packages paths. - /// - /// This variant is mainly intended for testing where we want to skip resolving `site-packages` - /// because it would unnecessarily complicate the test setup. - Testing(Vec), -} - -impl PythonEnvironmentPath { - pub fn explicit(path: impl Into, origin: SysPrefixPathOrigin) -> Self { - Self::Explicit(path.into(), origin) - } -} diff --git a/crates/ty_python_semantic/src/site_packages.rs b/crates/ty_python_semantic/src/site_packages.rs index 9c353e8ee6..d83ba72059 100644 --- a/crates/ty_python_semantic/src/site_packages.rs +++ b/crates/ty_python_semantic/src/site_packages.rs @@ -11,8 +11,11 @@ use std::io; use std::num::NonZeroUsize; use std::ops::Deref; +use std::str::FromStr; use std::{fmt, sync::Arc}; +use crate::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource}; +use camino::Utf8Component; use indexmap::IndexSet; use ruff_annotate_snippets::{Level, Renderer, Snippet}; use ruff_db::system::{System, SystemPath, SystemPathBuf}; @@ -21,8 +24,6 @@ use ruff_python_trivia::Cursor; use ruff_source_file::{LineIndex, OneIndexed, SourceCode}; use ruff_text_size::{TextLen, TextRange}; -use crate::{PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource}; - type SitePackagesDiscoveryResult = Result; /// An ordered, deduplicated set of `site-packages` search paths. @@ -43,13 +44,9 @@ type SitePackagesDiscoveryResult = Result; /// *might* be added to the `SitePackagesPaths` twice, but we wouldn't /// want duplicates to appear in this set. #[derive(Debug, PartialEq, Eq, Default)] -pub(crate) struct SitePackagesPaths(IndexSet); +pub struct SitePackagesPaths(IndexSet); impl SitePackagesPaths { - pub(crate) fn len(&self) -> usize { - self.0.len() - } - fn single(path: SystemPathBuf) -> Self { Self(IndexSet::from([path])) } @@ -61,6 +58,54 @@ impl SitePackagesPaths { fn extend(&mut self, other: Self) { self.0.extend(other.0); } + + /// Tries to detect the version from the layout of the `site-packages` directory. + pub fn python_version_from_layout(&self) -> Option { + if cfg!(windows) { + // The path to `site-packages` on Unix is + // `/lib/pythonX.Y/site-packages`, + // but on Windows it's `/Lib/site-packages`. + return None; + } + + let primary_site_packages = self.0.first()?; + + let mut site_packages_ancestor_components = + primary_site_packages.components().rev().skip(1).map(|c| { + // This should have all been validated in `site_packages.rs` + // when we resolved the search paths for the project. + debug_assert!( + matches!(c, Utf8Component::Normal(_)), + "Unexpected component in site-packages path `{c:?}` \ + (expected `site-packages` to be an absolute path with symlinks resolved, \ + located at `/lib/pythonX.Y/site-packages`)" + ); + + c.as_str() + }); + + let parent_component = site_packages_ancestor_components.next()?; + + if site_packages_ancestor_components.next()? != "lib" { + return None; + } + + let version = parent_component + .strip_prefix("python") + .or_else(|| parent_component.strip_prefix("pypy"))? + .trim_end_matches('t'); + + let version = PythonVersion::from_str(version).ok()?; + let source = PythonVersionSource::InstallationDirectoryLayout { + site_packages_parent_dir: Box::from(parent_component), + }; + + Some(PythonVersionWithSource { version, source }) + } + + pub fn into_vec(self) -> Vec { + self.0.into_iter().collect() + } } impl FromIterator for SitePackagesPaths { @@ -85,13 +130,67 @@ impl PartialEq<&[SystemPathBuf]> for SitePackagesPaths { } #[derive(Debug)] -pub(crate) enum PythonEnvironment { +pub enum PythonEnvironment { Virtual(VirtualEnvironment), System(SystemEnvironment), } impl PythonEnvironment { - pub(crate) fn new( + pub fn discover( + project_root: &SystemPath, + system: &dyn System, + ) -> Result, SitePackagesDiscoveryError> { + fn resolve_environment( + system: &dyn System, + path: &SystemPath, + origin: SysPrefixPathOrigin, + ) -> Result { + tracing::debug!("Resolving {origin}: {path}"); + PythonEnvironment::new(path, origin, system) + } + + if let Ok(virtual_env) = system.env_var("VIRTUAL_ENV") { + return resolve_environment( + system, + SystemPath::new(&virtual_env), + SysPrefixPathOrigin::VirtualEnvVar, + ) + .map(Some); + } + + if let Ok(conda_env) = system.env_var("CONDA_PREFIX") { + return resolve_environment( + system, + SystemPath::new(&conda_env), + SysPrefixPathOrigin::CondaPrefixVar, + ) + .map(Some); + } + + tracing::debug!("Discovering virtual environment in `{project_root}`"); + let virtual_env_directory = project_root.join(".venv"); + + match PythonEnvironment::new( + &virtual_env_directory, + SysPrefixPathOrigin::LocalVenv, + system, + ) { + Ok(environment) => return Ok(Some(environment)), + Err(err) => { + if system.is_directory(&virtual_env_directory) { + tracing::debug!( + "Ignoring automatically detected virtual environment at `{}`: {}", + &virtual_env_directory, + err + ); + } + } + } + + Ok(None) + } + + pub fn new( path: impl AsRef, origin: SysPrefixPathOrigin, system: &dyn System, @@ -111,23 +210,17 @@ impl PythonEnvironment { } } - /// Returns the `site-packages` directories for this Python environment, - /// as well as the Python version that was used to create this environment - /// (the latter will only be available for virtual environments that specify + /// Returns the Python version that was used to create this environment + /// (will only be available for virtual environments that specify /// the metadata in their `pyvenv.cfg` files). - /// - /// See the documentation for [`site_packages_directory_from_sys_prefix`] for more details. - pub(crate) fn into_settings( - self, - system: &dyn System, - ) -> SitePackagesDiscoveryResult<(SitePackagesPaths, Option)> { - Ok(match self { - Self::Virtual(venv) => (venv.site_packages_directories(system)?, venv.version), - Self::System(env) => (env.site_packages_directories(system)?, None), - }) + pub fn python_version_from_metadata(&self) -> Option<&PythonVersionWithSource> { + match self { + Self::Virtual(venv) => venv.version.as_ref(), + Self::System(_) => None, + } } - fn site_packages_directories( + pub fn site_packages_paths( &self, system: &dyn System, ) -> SitePackagesDiscoveryResult { @@ -173,7 +266,7 @@ impl PythonImplementation { /// The format of this file is not defined anywhere, and exactly which keys are present /// depends on the tool that was used to create the virtual environment. #[derive(Debug)] -pub(crate) struct VirtualEnvironment { +pub struct VirtualEnvironment { root_path: SysPrefixPath, base_executable_home_path: PythonHomePath, include_system_site_packages: bool, @@ -322,7 +415,7 @@ impl VirtualEnvironment { ); if let Some(parent_env_site_packages) = parent_environment.as_deref() { - match parent_env_site_packages.site_packages_directories(system) { + match parent_env_site_packages.site_packages_paths(system) { Ok(parent_environment_site_packages) => { site_packages_directories.extend(parent_environment_site_packages); } @@ -492,7 +585,7 @@ struct RawPyvenvCfg<'s> { /// This environment may or may not be one that is managed by the operating system itself, e.g., /// this captures both Homebrew-installed Python versions and the bundled macOS Python installation. #[derive(Debug)] -pub(crate) struct SystemEnvironment { +pub struct SystemEnvironment { root_path: SysPrefixPath, } @@ -1599,7 +1692,7 @@ mod tests { // directory let env = PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system).unwrap(); - let site_packages = env.site_packages_directories(&system); + let site_packages = env.site_packages_paths(&system); if cfg!(unix) { assert!( matches!( @@ -1638,7 +1731,7 @@ mod tests { // Environment creation succeeds, but site-packages retrieval fails let env = PythonEnvironment::new("/env", SysPrefixPathOrigin::PythonCliFlag, &system).unwrap(); - let site_packages = env.site_packages_directories(&system); + let site_packages = env.site_packages_paths(&system); assert!( matches!( site_packages, diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 7107f19950..4834771e78 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -21,7 +21,7 @@ use std::fmt::Write; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::types::check_types; use ty_python_semantic::{ - Program, ProgramSettings, PythonEnvironmentPath, PythonPlatform, PythonVersionSource, + Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SysPrefixPathOrigin, }; @@ -259,6 +259,18 @@ fn run_test( let configuration = test.configuration(); + let site_packages_paths = if let Some(python) = configuration.python() { + let environment = + PythonEnvironment::new(python, SysPrefixPathOrigin::PythonCliFlag, db.system()) + .expect("Python environment to point to a valid path"); + environment + .site_packages_paths(db.system()) + .expect("Python environment to be valid") + .into_vec() + } else { + vec![] + }; + let settings = ProgramSettings { python_version: PythonVersionWithSource { version: python_version, @@ -271,15 +283,7 @@ fn run_test( src_roots: vec![src_path], extra_paths: configuration.extra_paths().unwrap_or_default().to_vec(), custom_typeshed: custom_typeshed_path.map(SystemPath::to_path_buf), - python_environment: configuration - .python() - .map(|sys_prefix| { - PythonEnvironmentPath::explicit( - sys_prefix.to_path_buf(), - SysPrefixPathOrigin::PythonCliFlag, - ) - }) - .unwrap_or(PythonEnvironmentPath::Testing(vec![])), + site_packages_paths, } .to_search_paths(db.system(), db.vendored()) .expect("Failed to resolve search path settings"),