diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index e7aac62645..488218b548 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -4,7 +4,9 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use dashmap::mapref::entry::Entry; -pub use directory::{DirectoryListing, DirectoryListingError, directory_listing}; +pub use directory::{ + DirectoryListing, DirectoryListingError, directory_listing, system_path_to_directory, +}; pub use file_root::{FileRoot, FileRootKind}; pub use path::FilePath; use ruff_notebook::{Notebook, NotebookError}; diff --git a/crates/ty_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs index e752319e87..b338fff7ec 100644 --- a/crates/ty_project/src/db/changes.rs +++ b/crates/ty_project/src/db/changes.rs @@ -98,7 +98,10 @@ impl ProjectDatabase { let configuration_paths = ConfigurationPaths::from_metadata(metadata); // The initial uv metadata request may have failed before a workspace could be discovered. let uv_enabled = metadata.use_uv().workspace_discovery_enabled(); - let virtual_environment = project.program_settings(self).virtual_environment.clone(); + let virtual_environment = project + .program_settings(self) + .virtual_environment() + .map(SystemPath::to_path_buf); let python_path = metadata.configured_python_path(self.system()).or_else(|| { PythonEnvironment::virtual_environment_candidate(Some(&project_root), self.system()) .map(|(path, _)| SystemPath::absolute(path, self.system().current_directory())) @@ -423,6 +426,7 @@ impl ProjectDatabase { } Err(error) => { tracing::error!("Failed to resolve program settings: {error}"); + project.update_environment_error(self, &error); } } } diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 2d2fec4f7f..f6eb4362a3 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -3,7 +3,7 @@ reason = "Prefer System trait methods over std methods in ty crates" )] use crate::glob::{GlobFilterCheckMode, IncludeResult}; -use crate::metadata::options::OptionDiagnostic; +use crate::metadata::options::{OptionDiagnostic, ToProgramSettingsError}; use crate::parallel::ParallelIteratorExt; use crate::script::Script; use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; @@ -18,7 +18,7 @@ use rayon::prelude::*; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, }; -use ruff_db::files::{File, system_path_to_file}; +use ruff_db::files::{File, system_path_to_directory, system_path_to_file}; use ruff_db::parsed::parsed_module; use ruff_db::system::{SystemPath, SystemPathBuf, deduplicate_nested_paths}; use rustc_hash::FxHashSet; @@ -30,7 +30,7 @@ use std::iter::FusedIterator; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; use ty_python_core::ProgramFile; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings, PythonEnvironmentError}; pub use ty_python_semantic::Db as SemanticDb; use ty_python_semantic::dependency::{DependencyMetadata, DependencyProjectKind}; use ty_python_semantic::lint::RuleSelection; @@ -278,7 +278,7 @@ impl Project { Program::from_settings(db, self.program_settings(db)) } - /// Extract dependency information once per metadata update. Unrelated project settings and + /// Extract dependency information for the resolved Python environment. Unrelated settings and /// source ranges do not invalidate import inference when the extracted information is equal. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn dependency_metadata( @@ -296,28 +296,41 @@ impl Project { let environment = workspace .environment() .ok_or(DependencyMetadataError::MissingEnvironment)?; - let environment = db - .system() - .canonicalize_path(environment) - .map_err(|error| DependencyMetadataError::InvalidEnvironment { - path: environment.to_path_buf(), - message: error.to_string().into(), - })?; - let selected_environment = metadata - .to_merged_options() - .python_environment(db.system()) - .map_err(|error| { - DependencyMetadataError::EnvironmentResolution(error.to_string().into()) - })? + // Track the path before resolving it, so a synced change can invalidate the result. + if let Ok(directory) = system_path_to_directory(db, environment) { + let _ = directory.revision(db); + } + let canonical_environment = + db.system() + .canonicalize_path(environment) + .map_err(|error| { + // If `.venv` points to `env`, deleting `env` makes this fail before we read + // the target's directory status below. Salsa then drops that dependency, + // so recreating `env` and syncing only its path won't invalidate the cached + // error. Retry on the next revision so dependency checks can recover. + db.report_untracked_read(); + DependencyMetadataError::InvalidEnvironment { + path: environment.to_path_buf(), + message: error.to_string().into(), + } + })?; + // Changes may be reported for the canonical target instead of the symlink. + let _ = system_path_to_directory(db, &canonical_environment); + let program_settings = self.program_settings(db); + let selected_environment = program_settings + .python_environment + .as_ref() + .map_err(|error| DependencyMetadataError::EnvironmentResolution(error.message.clone()))? + .as_ref() .ok_or(DependencyMetadataError::MissingSelectedEnvironment)?; // An explicit Python environment can override uv's selection. Its installed modules may // belong to different distributions, so uv's ownership map cannot describe those imports. - if selected_environment.sys_prefix().as_std_path() != environment.as_std_path() { + if selected_environment.sys_prefix().as_std_path() != canonical_environment.as_std_path() { return Err(DependencyMetadataError::EnvironmentMismatch { selected: selected_environment.sys_prefix().to_path_buf(), selected_origin: selected_environment.origin().to_string().into(), - uv: environment, + uv: canonical_environment, }); } @@ -333,6 +346,23 @@ impl Project { } } + /// Update or clear the environment error while retaining the last working search paths. + fn update_environment_error(self, db: &mut dyn Db, error: &ToProgramSettingsError) { + let mut settings = self.program_settings(db).clone(); + let last_usable = match settings.python_environment { + Ok(environment) => environment, + Err(error) => error.last_usable, + }; + settings.python_environment = match error { + ToProgramSettingsError::PythonEnvironment(error) => Err(PythonEnvironmentError { + message: error.to_string().into(), + last_usable, + }), + _ => Ok(last_usable), + }; + self.update_program(db, settings); + } + pub fn root(self, db: &dyn Db) -> &SystemPath { self.metadata(db).root() } @@ -402,6 +432,7 @@ impl Project { "Failed to convert metadata to program settings, \ continuing without applying them: {error}" ); + self.update_environment_error(db, &error); Vec::new() } }; diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index 7637b00c87..a1a28e4a87 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -654,11 +654,16 @@ mod tests { use anyhow::{Context, anyhow}; use insta::assert_ron_snapshot; + use ruff_db::Db as _; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; + use ruff_db::files::File; + #[cfg(unix)] + use ruff_db::system::{OsSystem, System, SystemPath, WritableSystem}; use ruff_db::system::{SystemPathBuf, TestSystem}; use ruff_db::testing::assert_function_query_was_not_run_by_name; use ruff_python_ast::PythonVersion; use ruff_ranged_value::ValueSource; + use ty_python_core::program::UseDefaultStrategy; use ty_python_semantic::PythonVersionSource; use crate::db::{ProjectDatabase, testing::TestDb}; @@ -666,6 +671,8 @@ mod tests { Options, python_version::SupportedPythonVersion, uv::UvMetadata, value::RelativePathBuf, }; use crate::uv::{DependencyMetadataError, UvWorkspace}; + #[cfg(unix)] + use crate::watch::{ChangeEvent, CreatedKind, DeletedKind}; use crate::{Db as _, ProjectMetadata, ProjectMetadataError}; /// Without a `pyproject.toml` or `ty.toml`, the selected directory is the project root. @@ -1155,6 +1162,312 @@ unclosed table, expected `]` Ok(()) } + #[test] + fn dependency_metadata_tracks_environment_changes() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from(if cfg!(windows) { "C:/app" } else { "/app" }); + let uv_environment = root.join("uv-venv"); + let selected_environment = root.join("selected-venv"); + system + .memory_file_system() + .create_directory_all(&uv_environment)?; + let input = serde_json::json!({ + "schema": {"version": "preview"}, + "workspace_root": root, + "environment": {"root": uv_environment}, + }); + let mut metadata = ProjectMetadata::new("app", root); + metadata.apply_uv_workspace( + &system, + UvWorkspace { + metadata: Some(UvMetadata::from_metadata( + &serde_json::to_vec(&input)?, + &system, + )?), + error: None, + }, + )?; + metadata.set_override_options(Options::from_toml_str( + &format!("[environment]\npython = '{selected_environment}'"), + ValueSource::Cli, + )?); + let mut db = ProjectDatabase::use_defaults(metadata, system.clone()); + let project = db.project(); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::EnvironmentResolution(_)) + ); + + // Creating the selected environment refreshes program settings without changing metadata. + system.memory_file_system().write_file_all( + selected_environment.join("pyvenv.cfg"), + "home = /missing\nversion = 3.13.0\ninclude-system-site-packages = false", + )?; + system + .memory_file_system() + .create_directory_all(selected_environment.join(if cfg!(windows) { + "Lib/site-packages" + } else { + "lib/python3.13/site-packages" + }))?; + let (settings, _) = project + .metadata(&db) + .to_merged_options() + .to_program_settings(&system, db.vendored(), &UseDefaultStrategy)?; + project.update_program(&mut db, settings); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::EnvironmentMismatch { selected, .. }) + if selected == &selected_environment + ); + + // Directory availability is tracked independently of the resolved program settings. + system + .memory_file_system() + .remove_directory(&uv_environment)?; + File::sync_path(&mut db, &uv_environment); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::InvalidEnvironment { .. }) + ); + + system + .memory_file_system() + .create_directory_all(&uv_environment)?; + File::sync_path(&mut db, &uv_environment); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::EnvironmentMismatch { .. }) + ); + + Ok(()) + } + + #[test] + #[cfg(unix)] + fn dependency_metadata_tracks_environment_symlink() -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + let root = SystemPath::from_std_path(temp.path()).context("non-UTF-8 temporary path")?; + let system = OsSystem::new(root); + let root = system.canonicalize_path(root)?; + let environment = root.join("environment"); + let link = root.join(".venv"); + system.create_directory_all(&environment.join("lib/python3.13/site-packages"))?; + system.write_file( + &environment.join("pyvenv.cfg"), + "home = /missing\nversion = 3.13.0\ninclude-system-site-packages = false", + )?; + std::os::unix::fs::symlink(&environment, &link)?; + let input = serde_json::json!({ + "schema": {"version": "preview"}, + "workspace_root": root, + "environment": {"root": link}, + "members": [{"id": "app", "name": "app", "path": root}], + "resolution": {"app": {"kind": "package", "name": "app", "dependencies": []}}, + }); + let mut metadata = ProjectMetadata::new("app", root); + metadata.apply_uv_workspace( + &system, + UvWorkspace { + metadata: Some(UvMetadata::from_metadata( + &serde_json::to_vec(&input)?, + &system, + )?), + error: None, + }, + )?; + let mut db = ProjectDatabase::fallible(metadata, system.clone())?; + let project = db.project(); + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + + // The selected prefix is canonical, but uv's original path must remain available too. + std::fs::remove_file(&link)?; + File::sync_path(&mut db, &link); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::InvalidEnvironment { .. }) + ); + + std::os::unix::fs::symlink(&environment, &link)?; + File::sync_path(&mut db, &link); + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + + // Syncing only the target must invalidate the query without refreshing program settings. + std::fs::remove_dir_all(&environment)?; + File::sync_path(&mut db, &environment); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::InvalidEnvironment { .. }) + ); + + system.create_directory_all(&environment)?; + File::sync_path(&mut db, &environment); + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + + Ok(()) + } + + #[test] + #[cfg(unix)] + fn dependency_metadata_tracks_failed_selected_environment_refresh() -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + let root = SystemPath::from_std_path(temp.path()).context("non-UTF-8 temporary path")?; + let system = OsSystem::new(root); + let root = system.canonicalize_path(root)?; + let environment = root.join("environment"); + let uv_link = root.join(".venv"); + let selected_link = root.join("selected"); + system.create_directory_all(&environment.join("lib/python3.13/site-packages"))?; + system.write_file( + &environment.join("pyvenv.cfg"), + "home = /missing\nversion = 3.13.0\ninclude-system-site-packages = false", + )?; + std::os::unix::fs::symlink(&environment, &uv_link)?; + std::os::unix::fs::symlink(&environment, &selected_link)?; + let input = serde_json::json!({ + "schema": {"version": "preview"}, + "workspace_root": root, + "environment": {"root": uv_link}, + "members": [{"id": "app", "name": "app", "path": root}], + "resolution": {"app": {"kind": "package", "name": "app", "dependencies": []}}, + }); + let mut metadata = ProjectMetadata::new("app", root.clone()); + metadata.apply_uv_workspace( + &system, + UvWorkspace { + metadata: Some(UvMetadata::from_metadata( + &serde_json::to_vec(&input)?, + &system, + )?), + error: None, + }, + )?; + metadata.set_override_options(Options::from_toml_str( + &format!("[environment]\npython = '{selected_link}'"), + ValueSource::Cli, + )?); + let mut db = ProjectDatabase::fallible(metadata, system)?; + let project = db.project(); + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + let search_paths = project.program_settings(&db).search_paths.clone(); + + // Repeated failures must retain the checking program and the environment used for watching. + std::fs::remove_file(&selected_link)?; + for _ in 0..2 { + let workspace = project.metadata(&db).uv_workspace().clone(); + project.rediscover(&mut db, &root, workspace)?; + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::EnvironmentResolution(message)) + if message.contains(selected_link.as_str()) + ); + assert_eq!(project.program_settings(&db).search_paths, search_paths); + assert_eq!( + project.program_settings(&db).virtual_environment(), + Some(environment.as_path()), + ); + } + + std::os::unix::fs::symlink(&environment, &selected_link)?; + // A different settings error must clear the stale environment error, too. + let system = OsSystem::new(&root); + system.create_directory_all(&root.join("broken-typeshed/stdlib"))?; + system.write_file( + &root.join("pyproject.toml"), + "[tool.ty.environment]\ntypeshed = 'broken-typeshed'\n", + )?; + let workspace = project.metadata(&db).uv_workspace().clone(); + project.rediscover(&mut db, &root, workspace)?; + assert_matches!( + project + .metadata(&db) + .to_merged_options() + .to_program_settings(db.system(), db.vendored(), &super::FallibleStrategy), + Err(super::ToProgramSettingsError::SearchPaths(_)) + ); + assert_eq!(project.program_settings(&db).search_paths, search_paths); + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + + std::fs::remove_file(root.join("pyproject.toml"))?; + let workspace = project.metadata(&db).uv_workspace().clone(); + project.rediscover(&mut db, &root, workspace)?; + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + + std::fs::remove_file(&selected_link)?; + db.apply_changes(&[ChangeEvent::Deleted { + path: selected_link.clone(), + kind: DeletedKind::File, + }]); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::EnvironmentResolution(_)) + ); + + std::os::unix::fs::symlink(&environment, &selected_link)?; + db.apply_changes(&[ChangeEvent::Created { + path: selected_link, + kind: CreatedKind::Any, + }]); + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + + Ok(()) + } + + #[test] + #[cfg(unix)] + fn dependency_metadata_rechecks_uv_symlink_after_program_change() -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + let root = SystemPath::from_std_path(temp.path()).context("non-UTF-8 temporary path")?; + let system = OsSystem::new(root); + let root = system.canonicalize_path(root)?; + let old_environment = root.join("old"); + let selected_environment = root.join("selected"); + let uv_link = root.join(".venv"); + system.create_directory_all(&old_environment)?; + system.create_directory_all(&selected_environment.join("lib/python3.13/site-packages"))?; + system.write_file( + &selected_environment.join("pyvenv.cfg"), + "home = /missing\nversion = 3.13.0\ninclude-system-site-packages = false", + )?; + std::os::unix::fs::symlink(&old_environment, &uv_link)?; + let input = serde_json::json!({ + "schema": {"version": "preview"}, + "workspace_root": root, + "environment": {"root": uv_link}, + "members": [{"id": "app", "name": "app", "path": root}], + "resolution": {"app": {"kind": "package", "name": "app", "dependencies": []}}, + }); + let input = serde_json::to_vec(&input)?; + let mut metadata = ProjectMetadata::new("app", root); + metadata.apply_uv_workspace( + &system, + UvWorkspace { + metadata: Some(UvMetadata::from_metadata(&input, &system)?), + error: None, + }, + )?; + metadata.set_override_options(Options::from_toml_str( + &format!("[environment]\npython = '{selected_environment}'"), + ValueSource::Cli, + )?); + let mut db = ProjectDatabase::fallible(metadata, system)?; + let project = db.project(); + assert_matches!( + project.dependency_metadata(&db), + Err(DependencyMetadataError::EnvironmentMismatch { .. }) + ); + + std::fs::remove_file(&uv_link)?; + std::os::unix::fs::symlink(&selected_environment, &uv_link)?; + // An unrelated program change invalidates the query without refreshing uv metadata. + let mut settings = project.program_settings(&db).clone(); + settings.python_version.version = PythonVersion::PY312; + project.update_program(&mut db, settings); + assert_matches!(project.dependency_metadata(&db), Ok(Some(_))); + + Ok(()) + } + /// A uv refresh failure is reported without a second warning about missing dependency metadata. #[test] fn uv_refresh_error_takes_precedence_over_dependency_error() -> anyhow::Result<()> { diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 10d34598a0..a86caff619 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -35,7 +35,7 @@ use ty_module_resolver::{ ModuleGlobSet, ModuleGlobSetBuilder, SearchPathSettings, SearchPathSettingsError, SearchPaths, }; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{MisconfigurationStrategy, ProgramSettings}; +use ty_python_core::program::{MisconfigurationStrategy, ProgramSettings, PythonEnvironmentError}; use ty_python_semantic::lint::{Level, LintSource, RuleSelection}; use ty_python_semantic::{ AnalysisSettings, PythonEnvironment, PythonVersionFileSource, PythonVersionSource, @@ -216,6 +216,14 @@ impl Options { configured => configured.map_err(ToProgramSettingsError::PythonEnvironment), }; + let python_environment_error = match &python_environment { + Err(ToProgramSettingsError::PythonEnvironment(error)) => Some(PythonEnvironmentError { + message: error.to_string().into(), + last_usable: None, + }), + _ => None, + }; + // If in safe-mode, fallback to None if this fails instead of erroring. let python_environment = strategy .fallback_opt(python_environment, |_| { @@ -306,9 +314,10 @@ impl Options { python_version, python_platform, search_paths, - virtual_environment: python_environment - .filter(PythonEnvironment::is_virtual) - .map(|environment| environment.sys_prefix().to_path_buf()), + python_environment: match python_environment_error { + Some(error) => Err(error), + None => Ok(python_environment), + }, }, diagnostics, )) diff --git a/crates/ty_project/src/watch/project_watcher.rs b/crates/ty_project/src/watch/project_watcher.rs index 5a4cca77ff..d847bf2eab 100644 --- a/crates/ty_project/src/watch/project_watcher.rs +++ b/crates/ty_project/src/watch/project_watcher.rs @@ -252,8 +252,7 @@ pub fn watch_paths(db: &dyn Db, project: Project) -> WatchPaths { .map(UvMetadata::workspace_root); let virtual_environment = project .program_settings(db) - .virtual_environment - .as_deref() + .virtual_environment() .filter(|environment| !environment.starts_with(project_path)); // Watch the workspace and project roots and any paths provided by the user on the CLI. diff --git a/crates/ty_python_core/src/db.rs b/crates/ty_python_core/src/db.rs index 42459247c9..4a8f18ae2e 100644 --- a/crates/ty_python_core/src/db.rs +++ b/crates/ty_python_core/src/db.rs @@ -166,7 +166,7 @@ pub(crate) mod tests { .context("Failed to write test files")?; let program_settings = ProgramSettings { - virtual_environment: None, + python_environment: Ok(None), python_version: PythonVersionWithSource { version: self.python_version, source: PythonVersionSource::default(), diff --git a/crates/ty_python_core/src/program.rs b/crates/ty_python_core/src/program.rs index a03dedd09c..119ce7c947 100644 --- a/crates/ty_python_core/src/program.rs +++ b/crates/ty_python_core/src/program.rs @@ -1,11 +1,13 @@ use crate::{Db, platform::PythonPlatform}; +use std::fmt; + use ruff_db::files::File; -use ruff_db::system::{SystemPath, SystemPathBuf}; +use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; use ty_module_resolver::{ResolverEnvironment, SearchPaths}; -use ty_site_packages::PythonVersionWithSource; +use ty_site_packages::{PythonEnvironment, PythonVersionWithSource}; use crate::ProgramFile; @@ -30,7 +32,7 @@ impl<'db> Program<'db> { python_version, python_platform, search_paths, - virtual_environment: _, + python_environment: _, } = settings; let resolver_environment = @@ -60,11 +62,9 @@ pub struct ProgramSettings { pub python_version: PythonVersionWithSource, pub python_platform: PythonPlatform, pub search_paths: SearchPaths, - /// The root of the resolved virtual environment, if any. File watchers use this to observe - /// `pyvenv.cfg` and directory changes without resolving the environment again. System Python - /// installations are very unlikely to be deleted and recreated, so we exclude them to avoid - /// recursively watching a system prefix such as `/usr`. - pub virtual_environment: Option, + /// The resolved Python environment. Queries must use this instead of resolving the environment + /// again, so that changes invalidate their cached results. + pub python_environment: Result, PythonEnvironmentError>, } impl ProgramSettings { @@ -73,7 +73,36 @@ impl ProgramSettings { python_version: PythonVersionWithSource::default(), python_platform: PythonPlatform::default(), search_paths: SearchPaths::empty(vendored), - virtual_environment: None, + python_environment: Ok(None), } } + + /// The root of the resolved virtual environment, for watching `pyvenv.cfg` and directory changes. + /// System installations are unlikely to be recreated, and watching a prefix such as `/usr` + /// recursively would be expensive. + pub fn virtual_environment(&self) -> Option<&SystemPath> { + let environment = match &self.python_environment { + Ok(environment) => environment, + Err(error) => &error.last_usable, + }; + environment + .as_ref() + .filter(|environment| environment.is_virtual()) + .map(|environment| &**environment.sys_prefix()) + } } + +/// A failed environment selection that retains the previous environment for watching and recovery. +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)] +pub struct PythonEnvironmentError { + pub message: Box, + pub last_usable: Option, +} + +impl fmt::Display for PythonEnvironmentError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.message.fmt(f) + } +} + +impl std::error::Error for PythonEnvironmentError {} diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index 571fa4e242..fd7984ed70 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -318,9 +318,7 @@ pub(crate) mod tests { }; let program_settings = ProgramSettings { - virtual_environment: self - .third_party_packages - .then(|| SystemPathBuf::from("/.venv")), + python_environment: Ok(None), python_version: PythonVersionWithSource { version: self.python_version, source: PythonVersionSource::default(), diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index 2bb387857a..11f6e4aed3 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -270,7 +270,7 @@ impl PartialEq<&[SystemPathBuf]> for SitePackagesPaths { } } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub enum PythonEnvironment { Virtual(VirtualEnvironment), System(SystemEnvironment), @@ -493,7 +493,7 @@ impl std::fmt::Display for InstallationDir { /// /// We only need to distinguish cases that change the on-disk layout. /// Everything else can be treated like CPython. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, get_size2::GetSize)] pub(crate) enum PythonImplementation { CPython, PyPy, @@ -520,7 +520,7 @@ impl PythonImplementation { } } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, get_size2::GetSize)] struct PythonInterpreterLayout { version: Option, implementation: PythonImplementation, @@ -649,7 +649,7 @@ impl PythonInterpreterLayout { } } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, get_size2::GetSize)] enum PythonBuildVariant { #[default] Unknown, @@ -686,7 +686,7 @@ impl PythonBuildVariant { /// Most of this information is derived from the virtual environment's `pyvenv.cfg` file. /// 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)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct VirtualEnvironment { root_path: SysPrefixPath, base_executable_home_path: Option, @@ -1156,7 +1156,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)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct SystemEnvironment { path: PythonEnvironmentPath, } @@ -1861,7 +1861,7 @@ fn real_stdlib_directory_from_sys_prefix( /// `/opt/homebrew/lib/python3.X/site-packages`. /// /// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] pub struct SysPrefixPath { inner: SystemPathBuf, origin: SysPrefixPathOrigin, @@ -1887,7 +1887,7 @@ fn sys_prefix_from_executable_path(path: &SystemPath) -> Option<&SystemPath> { } /// A selected Python environment path resolved to its `sys.prefix`. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] enum PythonEnvironmentPath { Prefix(SysPrefixPath), Executable { @@ -2126,7 +2126,7 @@ impl Deref for SysPrefixPath { } /// Enumeration of sources a `sys.prefix` path can come from. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] pub enum SysPrefixPathOrigin { /// The `sys.prefix` path came from a configuration file setting: `pyproject.toml` or `ty.toml` ConfigFileSetting(Arc, Option), @@ -2258,7 +2258,7 @@ impl std::fmt::Display for SysPrefixPathOrigin { /// /// [`PYTHONHOME`]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME /// [the original PEP adding the `venv` module]: https://peps.python.org/pep-0405/ -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] struct PythonHomePath(SystemPathBuf); impl PythonHomePath { diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index be3849839e..74a7f7bfa0 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -282,9 +282,7 @@ fn run_test( .collect(); let settings = ProgramSettings { - virtual_environment: python_environment - .filter(PythonEnvironment::is_virtual) - .map(|environment| environment.sys_prefix().to_path_buf()), + python_environment: Ok(python_environment), python_version: PythonVersionWithSource { version: python_version, source: PythonVersionSource::Cli,