mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[ty] Invalidate dependency metadata when environments change (#29126)
`Project::dependency_metadata` resolves Python environments inside a Salsa query without tracking those inputs. This can leave invalid-environment, missing-environment, and environment-mismatch diagnostics stale after an environment changes. Store the selected environment's resolution result in `ProgramSettings` and track uv's environment directory through Salsa. Resolve uv's canonical path whenever the query runs so it observes symlink changes even when uv metadata is unchanged. After a failed selection, retain the last usable environment for watching and recovery, and preserve the checking program's search paths. Dependency metadata reports the current environment error. Fixes astral-sh/ty#4604.
This commit is contained in:
1 parent
35f1b2a242
commit
e0da489892
11 files changed
+436
-53
No files matched your search
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<SystemPathBuf>,
|
||||
/// 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<Option<PythonEnvironment>, 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<str>,
|
||||
pub last_usable: Option<PythonEnvironment>,
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonEnvironmentError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.message.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PythonEnvironmentError {}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<PythonVersion>,
|
||||
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<PythonHomePath>,
|
||||
@@ -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<SystemPathBuf>, Option<TextRange>),
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user