mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[ty] Refresh Python settings when virtual environments change (#28650)
## Summary This PR is a first step towards updating the project's uv metadata when the virtual environment changes. The scope of this PR is to add the non-uv specific virtual environment file watching. That is, it adds the necessary file watching handling to update the detected virtual environment and its Python version when: * a `pyenv.cfg` changes * the default `.venv` or the explicitly specified `environment.python` virtual environment gets created or deleted. Unfortunately, this does require storing the path to the resolved virtual environment in `ProgramSettings`. Otherwise, we'd have to resolve it every time we see a related file watcher event. It's unfortunate because it doesn't feel like a setting. But there isn't really any better to carry forward this information. I recommend reviewing this PR commit by commit. ## Test Plan Native file-watching regression tests pass.
This commit is contained in:
1 parent
85077a70e8
commit
226201fef0
19 files changed
+764
-332
No files matched your search
@@ -462,7 +462,7 @@ import colorama
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `colorama`
|
||||
@@ -473,7 +473,7 @@ import colorama
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
|
||||
Found 2 diagnostics
|
||||
@@ -495,7 +495,7 @@ import colorama
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
|
||||
error[unresolved-import]: Cannot resolve imported module `colorama`
|
||||
@@ -506,7 +506,7 @@ import colorama
|
||||
info: Searched in the following paths during module resolution:
|
||||
info: 1. <temp_dir>/project (first-party code)
|
||||
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages (site-packages)
|
||||
info: 3. <temp_dir>/opt/homebrew/Cellar/python@3.13/3.13.5/lib/python3.13/site-packages (site-packages)
|
||||
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
+530
-141
File diff suppressed because it is too large.
Load diff
@@ -419,9 +419,7 @@ mod tests {
|
||||
use ty_module_resolver::SearchPathSettings;
|
||||
use ty_project::{Db as _, ProjectMetadata, SemanticDb as _};
|
||||
use ty_python_core::ProgramFile;
|
||||
use ty_python_core::platform::PythonPlatform;
|
||||
use ty_python_core::program::{FallibleStrategy, ProgramSettings};
|
||||
use ty_python_semantic::PythonVersionWithSource;
|
||||
use ty_python_core::program::FallibleStrategy;
|
||||
|
||||
/// A way to create a simple single-file (named `main.py`) cursor test.
|
||||
///
|
||||
@@ -674,14 +672,10 @@ mod tests {
|
||||
.to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
|
||||
.expect("valid search paths");
|
||||
|
||||
db.project().update_program(
|
||||
&mut db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersionWithSource::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths,
|
||||
},
|
||||
);
|
||||
let project = db.project();
|
||||
let mut settings = project.program_settings(&db).clone();
|
||||
settings.search_paths = search_paths;
|
||||
project.update_program(&mut db, settings);
|
||||
|
||||
db.files()
|
||||
.try_add_root(&db, &project_root, FileRootKind::Project);
|
||||
|
||||
@@ -834,10 +834,12 @@ impl SearchPaths {
|
||||
let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len());
|
||||
|
||||
for path in site_packages_paths {
|
||||
// Resolve symlinks so site-packages outside the environment keeps its own file watch,
|
||||
// and imports use the same path as the reported changes.
|
||||
let path = canonicalize(path, system);
|
||||
tracing::debug!("Adding site-packages search path `{path}`");
|
||||
let path = strategy.fallback_opt(
|
||||
SearchPath::site_packages(system, path.clone())
|
||||
.map_err(SearchPathSettingsError::from),
|
||||
SearchPath::site_packages(system, path).map_err(SearchPathSettingsError::from),
|
||||
|err| {
|
||||
tracing::debug!("Skipping invalid site-packages search-path: {err}");
|
||||
},
|
||||
|
||||
@@ -740,7 +740,6 @@ pub(crate) mod testing {
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ty_module_resolver::SearchPathSettings;
|
||||
use ty_python_core::ProgramFile;
|
||||
use ty_python_core::platform::PythonPlatform;
|
||||
use ty_python_core::program::{FallibleStrategy, ProgramSettings};
|
||||
#[cfg(feature = "testing")]
|
||||
use ty_python_semantic::ProgramEnvironment;
|
||||
@@ -803,11 +802,8 @@ pub(crate) mod testing {
|
||||
|
||||
db.files().try_add_root(&db, &root, FileRootKind::Project);
|
||||
|
||||
let program_settings = ProgramSettings {
|
||||
python_version: PythonVersionWithSource::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths,
|
||||
};
|
||||
let mut program_settings = ProgramSettings::empty(db.vendored());
|
||||
program_settings.search_paths = search_paths;
|
||||
let project = Project::from_metadata(
|
||||
&db,
|
||||
project,
|
||||
@@ -821,16 +817,13 @@ pub(crate) mod testing {
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
pub fn set_python_version(&mut self, python_version: PythonVersion) {
|
||||
let program = self.project().program(self);
|
||||
let settings = ProgramSettings {
|
||||
python_version: PythonVersionWithSource {
|
||||
source: ty_python_semantic::PythonVersionSource::Default,
|
||||
version: python_version,
|
||||
},
|
||||
python_platform: program.python_platform(self).clone(),
|
||||
search_paths: program.search_paths(self).clone(),
|
||||
let project = self.project();
|
||||
let mut settings = project.program_settings(self).clone();
|
||||
settings.python_version = PythonVersionWithSource {
|
||||
source: ty_python_semantic::PythonVersionSource::Default,
|
||||
version: python_version,
|
||||
};
|
||||
self.project().update_program(self, settings);
|
||||
project.update_program(self, settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ use ruff_db::files::{File, Files, system_path_to_file};
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use rustc_hash::FxHashSet;
|
||||
use ty_python_core::program::FallibleStrategy;
|
||||
use ty_python_semantic::PythonEnvironment;
|
||||
|
||||
/// Represents the result of applying changes to the project database.
|
||||
pub struct ChangeResult {
|
||||
project_changed: bool,
|
||||
project_sync_path: Option<SystemPathBuf>,
|
||||
custom_stdlib_changed: bool,
|
||||
changed_files: ChangedFiles,
|
||||
}
|
||||
|
||||
@@ -32,11 +32,6 @@ impl ChangeResult {
|
||||
self.project_sync_path.as_deref()
|
||||
}
|
||||
|
||||
/// Returns `true` if the custom stdlib's VERSIONS file has changed.
|
||||
pub fn custom_stdlib_changed(&self) -> bool {
|
||||
self.custom_stdlib_changed
|
||||
}
|
||||
|
||||
/// Returns the scripts whose environments may need synchronization after these file events.
|
||||
///
|
||||
/// Returns no scripts if the project was unindexed when the changes were applied.
|
||||
@@ -99,6 +94,14 @@ impl ProjectDatabase {
|
||||
let project = self.project();
|
||||
let project_root = project.root(self).to_path_buf();
|
||||
let configuration_paths = ConfigurationPaths::from_metadata(project.metadata(self));
|
||||
let virtual_environment = project.program_settings(self).virtual_environment.clone();
|
||||
let python_path = project
|
||||
.metadata(self)
|
||||
.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()))
|
||||
});
|
||||
let program = self.project().program(self);
|
||||
let custom_stdlib_versions_path = program
|
||||
.custom_stdlib_search_path(self)
|
||||
@@ -107,7 +110,6 @@ impl ProjectDatabase {
|
||||
let mut result = ChangeResult {
|
||||
project_changed: false,
|
||||
project_sync_path: None,
|
||||
custom_stdlib_changed: false,
|
||||
changed_files: if project.file_set(self).is_lazy() {
|
||||
ChangedFiles::Unindexed
|
||||
} else {
|
||||
@@ -125,6 +127,7 @@ impl ProjectDatabase {
|
||||
let mut removed_paths = BTreeSet::default();
|
||||
let mut reload_project = false;
|
||||
let mut reload_project_files = false;
|
||||
let mut refresh_program_settings = false;
|
||||
// TODO: This should be removed once the incremental checker is ported
|
||||
// over to the `ignore` crate, since the `ignore` crate will respect
|
||||
// the settings provided in `create_walker`. ---AG
|
||||
@@ -138,6 +141,12 @@ impl ProjectDatabase {
|
||||
for change in changes {
|
||||
tracing::debug!("Handling file watcher change event: {:?}", change);
|
||||
|
||||
refresh_program_settings |= affects_python_environment(
|
||||
change,
|
||||
virtual_environment.as_deref(),
|
||||
python_path.as_deref(),
|
||||
);
|
||||
|
||||
if let Some(path) = change.system_path() {
|
||||
if configuration_paths.is_configuration(path, &project_root) {
|
||||
File::sync_path(self, path);
|
||||
@@ -190,7 +199,7 @@ impl ProjectDatabase {
|
||||
}
|
||||
|
||||
if Some(path) == custom_stdlib_versions_path.as_deref() {
|
||||
result.custom_stdlib_changed = true;
|
||||
refresh_program_settings = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +309,7 @@ impl ProjectDatabase {
|
||||
.as_ref()
|
||||
.is_some_and(|versions_path| versions_path.starts_with(path))
|
||||
{
|
||||
result.custom_stdlib_changed = true;
|
||||
refresh_program_settings = true;
|
||||
}
|
||||
|
||||
if configuration_paths.may_contain_configuration(path, &project_root) {
|
||||
@@ -383,7 +392,7 @@ impl ProjectDatabase {
|
||||
removed_paths.clear();
|
||||
}
|
||||
|
||||
if result.custom_stdlib_changed {
|
||||
if refresh_program_settings {
|
||||
let metadata = project.metadata(self);
|
||||
let merged_options = metadata.to_merged_options();
|
||||
match merged_options.to_program_settings(
|
||||
@@ -488,3 +497,70 @@ impl ConfigurationPaths {
|
||||
fn is_ignore_file(path: &SystemPath) -> bool {
|
||||
matches!(path.file_name(), Some(".gitignore" | ".ignore"))
|
||||
}
|
||||
|
||||
/// `pyvenv.cfg` records the environment's Python version and whether imports include system
|
||||
/// site-packages. Recreating an environment can change both without changing ty's configuration.
|
||||
///
|
||||
/// `python_path` is the configured environment or interpreter path,
|
||||
/// or a virtual environment path (e.g. `VIRTUAL_ENV` or the project's `.venv`).
|
||||
///
|
||||
/// uv writes `pyvenv.cfg` before creating `site-packages`. If these events arrive in separate
|
||||
/// batches, creating `site-packages` must retry resolution. A watcher may only report the creation
|
||||
/// of the `site-packages` parent `lib` or `lib/pythonX.Y` directory, so we also handle those
|
||||
/// directories (`lib64` on some Unix systems, `Lib` on Windows). We match `site-packages` by name;
|
||||
/// an unrelated directory only causes an extra refresh of the same settings.
|
||||
///
|
||||
/// Similar to `site-packages`, renaming a directory to `.venv` can change the inferred virtual
|
||||
/// environment without an event for `pyvenv.cfg`. That's why we need to rediscover the virtual
|
||||
/// environment when any ancestor path of a valid virtual environment location is created or deleted.
|
||||
fn affects_python_environment(
|
||||
change: &ChangeEvent,
|
||||
virtual_environment: Option<&SystemPath>,
|
||||
python_path: Option<&SystemPath>,
|
||||
) -> bool {
|
||||
let may_be_environment_root = |path: &SystemPath| {
|
||||
virtual_environment == Some(path)
|
||||
|| python_path.is_some_and(|python_path| python_path.starts_with(path))
|
||||
};
|
||||
|
||||
match change {
|
||||
ChangeEvent::Created { path, .. }
|
||||
| ChangeEvent::Changed { path, .. }
|
||||
| ChangeEvent::Deleted { path, .. }
|
||||
if path.file_name() == Some("pyvenv.cfg") =>
|
||||
{
|
||||
path.parent().is_some_and(may_be_environment_root)
|
||||
}
|
||||
// The configured path can be an interpreter file as well as an environment directory.
|
||||
ChangeEvent::Created { path, .. } | ChangeEvent::Deleted { path, .. }
|
||||
if python_path.is_some_and(|python_path| python_path.starts_with(path))
|
||||
|| virtual_environment.is_some_and(|environment| environment.starts_with(path)) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
ChangeEvent::Created {
|
||||
path,
|
||||
kind: CreatedKind::Directory,
|
||||
}
|
||||
| ChangeEvent::Deleted {
|
||||
path,
|
||||
kind: DeletedKind::Directory | DeletedKind::Any,
|
||||
} => {
|
||||
if path.file_name() == Some("site-packages") {
|
||||
return true;
|
||||
}
|
||||
|
||||
let is_library =
|
||||
|path: &SystemPath| matches!(path.file_name(), Some("lib" | "lib64" | "Lib"));
|
||||
let library = if is_library(path) {
|
||||
Some(path.as_path())
|
||||
} else {
|
||||
path.parent().filter(|parent| is_library(parent))
|
||||
};
|
||||
library
|
||||
.and_then(SystemPath::parent)
|
||||
.is_some_and(may_be_environment_root)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -549,6 +549,13 @@ impl ProjectMetadata {
|
||||
.chain(self.fallback_options.as_deref())
|
||||
}
|
||||
|
||||
/// Returns the configured environment or interpreter path, without resolving the full merged options.
|
||||
pub(crate) fn configured_python_path(&self, system: &dyn System) -> Option<SystemPathBuf> {
|
||||
self.options_in_precedence_order(&self.options, self.uv_workspace_options.as_deref())
|
||||
.find_map(|options| options.environment.as_ref()?.python.as_ref())
|
||||
.map(|path| path.absolute(self.root(), system))
|
||||
}
|
||||
|
||||
/// Loads the lower-precedence options from configuration files.
|
||||
///
|
||||
/// This includes:
|
||||
|
||||
@@ -306,6 +306,9 @@ impl Options {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths,
|
||||
virtual_environment: python_environment
|
||||
.filter(PythonEnvironment::is_virtual)
|
||||
.map(|environment| environment.sys_prefix().to_path_buf()),
|
||||
},
|
||||
diagnostics,
|
||||
))
|
||||
|
||||
@@ -178,16 +178,43 @@ impl WatchPaths {
|
||||
}
|
||||
}
|
||||
|
||||
/// Watches are registered in project, module, then configuration order. On Linux, the last
|
||||
/// registered watch determines the path reported for overlapping symlinks.
|
||||
/// Watches are registered in project, Python environment, module, then configuration order.
|
||||
/// On Linux, the last registered watch determines the path reported for overlapping symlinks.
|
||||
///
|
||||
/// Project roots and explicitly included paths come first because project files are discovered
|
||||
/// by walking them. Module search paths come next so imports are reported relative to their
|
||||
/// by walking them. A virtual environment outside the project needs its own watch because
|
||||
/// `pyvenv.cfg` can change the inferred Python version and import search paths. Watching its
|
||||
/// `site-packages` alone does not cover that file. System environment roots are excluded to
|
||||
/// avoid recursively watching a system prefix such as `/usr`.
|
||||
///
|
||||
/// Module search paths follow the environment so imports are reported relative to their
|
||||
/// search roots, rather than through symlinks inside the project. Configuration paths come last
|
||||
/// so their events use the explicit paths checked for configuration changes.
|
||||
///
|
||||
/// # Known limitations
|
||||
///
|
||||
/// These limitations apply to virtual environments outside the project directory unless another
|
||||
/// watch already covers the relevant paths. Environments inside the project are covered by the project watch.
|
||||
///
|
||||
/// - Creating an initially missing environment: If environment resolution fails, there is no
|
||||
/// resolved root to add to the watch list. Supporting later creation would require choosing an
|
||||
/// existing ancestor to watch from an unresolved `environment.python` path, which can name
|
||||
/// either a directory or an interpreter. We accept this limitation to avoid additional path
|
||||
/// discovery and watch-registration logic for environments that could not be resolved.
|
||||
/// - Deleting and recreating an environment: Deleting the environment can remove its native
|
||||
/// watch, so its recreation goes unnoticed. Watching a surviving parent would cover this, but
|
||||
/// our watches are recursive: the parent could contain every environment in a shared Poetry
|
||||
/// directory, or even be the user's home directory. We accept this limitation to avoid watching
|
||||
/// those unrelated directories. A nonrecursive parent watch would need additional logic to
|
||||
/// register the environment watch again after recreation.
|
||||
#[salsa::tracked(returns(ref))]
|
||||
pub fn watch_paths(db: &dyn Db, project: Project) -> WatchPaths {
|
||||
let project_path = project.root(db);
|
||||
let virtual_environment = project
|
||||
.program_settings(db)
|
||||
.virtual_environment
|
||||
.as_deref()
|
||||
.filter(|environment| !environment.starts_with(project_path));
|
||||
|
||||
// Watch both the project root and any paths provided by the user on the CLI (removing any redundant nested paths).
|
||||
// This is necessary to observe changes to files that are outside the project root.
|
||||
@@ -212,15 +239,16 @@ pub fn watch_paths(db: &dyn Db, project: Project) -> WatchPaths {
|
||||
}
|
||||
}
|
||||
|
||||
// The project watch covers search paths inside its root. Deduplicate the others so
|
||||
// shared or nested search paths are registered once in stable order.
|
||||
let unique_module_paths = ruff_db::system::deduplicate_nested_paths(
|
||||
search_paths
|
||||
.into_iter()
|
||||
.filter(|path| !path.starts_with(project_path)),
|
||||
);
|
||||
// The project and virtual environment watches cover search paths inside their roots.
|
||||
// Deduplicate the remaining paths so shared or nested search paths are registered once.
|
||||
let unique_module_paths =
|
||||
ruff_db::system::deduplicate_nested_paths(search_paths.into_iter().filter(|path| {
|
||||
!path.starts_with(project_path)
|
||||
&& virtual_environment.is_none_or(|environment| !path.starts_with(environment))
|
||||
}));
|
||||
|
||||
let paths: Vec<_> = included_paths
|
||||
.chain(virtual_environment)
|
||||
.chain(unique_module_paths)
|
||||
.chain(project.metadata(db).extra_configuration_paths())
|
||||
.map(SystemPath::to_path_buf)
|
||||
|
||||
@@ -166,6 +166,7 @@ pub(crate) mod tests {
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let program_settings = ProgramSettings {
|
||||
virtual_environment: None,
|
||||
python_version: PythonVersionWithSource {
|
||||
version: self.python_version,
|
||||
source: PythonVersionSource::default(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{Db, platform::PythonPlatform};
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::system::SystemPath;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ty_module_resolver::{ResolverEnvironment, SearchPaths};
|
||||
@@ -30,6 +30,7 @@ impl<'db> Program<'db> {
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths,
|
||||
virtual_environment: _,
|
||||
} = settings;
|
||||
|
||||
let resolver_environment =
|
||||
@@ -59,6 +60,11 @@ 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>,
|
||||
}
|
||||
|
||||
impl ProgramSettings {
|
||||
@@ -67,6 +73,7 @@ impl ProgramSettings {
|
||||
python_version: PythonVersionWithSource::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPaths::empty(vendored),
|
||||
virtual_environment: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,6 +318,9 @@ pub(crate) mod tests {
|
||||
};
|
||||
|
||||
let program_settings = ProgramSettings {
|
||||
virtual_environment: self
|
||||
.third_party_packages
|
||||
.then(|| SystemPathBuf::from("/.venv")),
|
||||
python_version: PythonVersionWithSource {
|
||||
version: self.python_version,
|
||||
source: PythonVersionSource::default(),
|
||||
|
||||
@@ -2235,11 +2235,10 @@ impl<'db> KnownClassLookupError<'db> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::tests::{TestDbBuilder, setup_db};
|
||||
use crate::{PythonVersionSource, PythonVersionWithSource};
|
||||
use strum::IntoEnumIterator;
|
||||
use ty_module_resolver::resolve_module_confident;
|
||||
use ty_python_core::TestProgramDb as _;
|
||||
use ty_python_core::program::{Program, ProgramSettings};
|
||||
use ty_python_core::program::Program;
|
||||
|
||||
#[test]
|
||||
fn known_class_roundtrip_from_str() {
|
||||
@@ -2335,22 +2334,12 @@ mod tests {
|
||||
classes.sort_unstable_by_key(|(_, version)| *version);
|
||||
|
||||
let mut program = db.program();
|
||||
let mut current_version = program.python_version(&db);
|
||||
let python_platform = program.python_platform(&db).clone();
|
||||
let search_paths = program.search_paths(&db).clone();
|
||||
let mut settings = db.program_settings().clone();
|
||||
|
||||
for (class, version_added) in classes {
|
||||
if version_added != current_version {
|
||||
let settings = ProgramSettings {
|
||||
python_version: PythonVersionWithSource {
|
||||
version: version_added,
|
||||
source: PythonVersionSource::default(),
|
||||
},
|
||||
python_platform: python_platform.clone(),
|
||||
search_paths: search_paths.clone(),
|
||||
};
|
||||
if version_added != settings.python_version.version {
|
||||
settings.python_version.version = version_added;
|
||||
program = Program::from_settings(&db, &settings);
|
||||
current_version = version_added;
|
||||
}
|
||||
|
||||
// Check the class can be looked up successfully
|
||||
|
||||
@@ -15,12 +15,11 @@ use ruff_python_ast::PythonVersion;
|
||||
use salsa::Database as _;
|
||||
use salsa::plumbing::AsId;
|
||||
use ty_python_core::definition::Definition;
|
||||
use ty_python_core::program::{Program, ProgramSettings};
|
||||
use ty_python_core::program::Program;
|
||||
use ty_python_core::scope::FileScopeId;
|
||||
use ty_python_core::{
|
||||
ProgramFile, TestProgramDb as _, global_scope, place_table, semantic_index, use_def_map,
|
||||
};
|
||||
use ty_site_packages::{PythonVersionSource, PythonVersionWithSource};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -119,39 +118,10 @@ fn same_file_at_different_python_versions() -> anyhow::Result<()> {
|
||||
db.write_dedented("src/py312_dependency.py", "value: int = 312")?;
|
||||
|
||||
let file = system_path_to_file(&db, "src/main.py").expect("file to exist");
|
||||
let default_program = db.program();
|
||||
let search_paths = default_program.search_paths(&db).clone();
|
||||
let python_platform = default_program.python_platform(&db).clone();
|
||||
let py311 = ProgramFile::new(
|
||||
&db,
|
||||
file,
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersionWithSource {
|
||||
version: PythonVersion::PY311,
|
||||
source: PythonVersionSource::Default,
|
||||
},
|
||||
python_platform: python_platform.clone(),
|
||||
search_paths: search_paths.clone(),
|
||||
},
|
||||
),
|
||||
);
|
||||
let py312 = ProgramFile::new(
|
||||
&db,
|
||||
file,
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersionWithSource {
|
||||
version: PythonVersion::PY312,
|
||||
source: PythonVersionSource::Default,
|
||||
},
|
||||
python_platform,
|
||||
search_paths,
|
||||
},
|
||||
),
|
||||
);
|
||||
let py311 = db.program().program_file(&db, file);
|
||||
let mut settings = db.program_settings().clone();
|
||||
settings.python_version.version = PythonVersion::PY312;
|
||||
let py312 = Program::from_settings(&db, &settings).program_file(&db, file);
|
||||
|
||||
let check = |file, expected_type, expect_invalid_syntax, expect_unresolved_import| {
|
||||
let diagnostics = crate::check_file_unwrap(&db, file);
|
||||
@@ -204,31 +174,16 @@ fn program_file_changes_with_python_version() -> anyhow::Result<()> {
|
||||
(program_file.as_id(), program_file.python_file(&db).as_id())
|
||||
};
|
||||
|
||||
let equivalent_program = Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: db.program_settings().python_version.clone(),
|
||||
python_platform: program.python_platform(&db).clone(),
|
||||
search_paths: program.search_paths(&db).clone(),
|
||||
},
|
||||
);
|
||||
let mut settings = db.program_settings().clone();
|
||||
let equivalent_program = Program::from_settings(&db, &settings);
|
||||
assert_eq!(program, equivalent_program);
|
||||
assert_eq!(
|
||||
program_file_id,
|
||||
equivalent_program.program_file(&db, file).as_id()
|
||||
);
|
||||
|
||||
let py312_program = Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersionWithSource {
|
||||
version: PythonVersion::PY312,
|
||||
source: PythonVersionSource::Default,
|
||||
},
|
||||
python_platform: program.python_platform(&db).clone(),
|
||||
search_paths: program.search_paths(&db).clone(),
|
||||
},
|
||||
);
|
||||
settings.python_version.version = PythonVersion::PY312;
|
||||
let py312_program = Program::from_settings(&db, &settings);
|
||||
|
||||
let program_file = py312_program.program_file(&db, file);
|
||||
assert_ne!(program_file_id, program_file.as_id());
|
||||
|
||||
@@ -180,8 +180,8 @@ fn picks_up_edits_to_unwatched_pth_files() -> Result<()> {
|
||||
assert_json_snapshot!(watcher_snapshot(&watches), @r#"
|
||||
[
|
||||
"file://<temp_dir>/src :: **",
|
||||
"file://<temp_dir>/old :: **",
|
||||
"file://<temp_dir>/venv/<site-packages> :: **"
|
||||
"file://<temp_dir>/venv :: **",
|
||||
"file://<temp_dir>/old :: **"
|
||||
]
|
||||
"#);
|
||||
server.open_text_document(main, &source, 1);
|
||||
@@ -218,8 +218,8 @@ fn picks_up_edits_to_unwatched_pth_files() -> Result<()> {
|
||||
assert_json_snapshot!(watcher_snapshot(&watches), @r#"
|
||||
[
|
||||
"file://<temp_dir>/src :: **",
|
||||
"file://<temp_dir>/old :: **",
|
||||
"file://<temp_dir>/venv/<site-packages> :: **"
|
||||
"file://<temp_dir>/venv :: **",
|
||||
"file://<temp_dir>/old :: **"
|
||||
]
|
||||
"#);
|
||||
server.acknowledge_request(request_id);
|
||||
@@ -230,8 +230,8 @@ fn picks_up_edits_to_unwatched_pth_files() -> Result<()> {
|
||||
assert_json_snapshot!(watcher_snapshot(&watches), @r#"
|
||||
[
|
||||
"file://<temp_dir>/src :: **",
|
||||
"file://<temp_dir>/new :: **",
|
||||
"file://<temp_dir>/venv/<site-packages> :: **"
|
||||
"file://<temp_dir>/venv :: **",
|
||||
"file://<temp_dir>/new :: **"
|
||||
]
|
||||
"#);
|
||||
assert_eq!(server.acknowledge_unregistration()?, stale_pth_id);
|
||||
|
||||
@@ -296,35 +296,16 @@ impl PythonEnvironment {
|
||||
PythonEnvironment::new(path, origin, system)
|
||||
}
|
||||
|
||||
if let Ok(virtual_env) = system.env_var(EnvVars::VIRTUAL_ENV) {
|
||||
return resolve_environment(
|
||||
system,
|
||||
SystemPath::new(&virtual_env),
|
||||
SysPrefixPathOrigin::VirtualEnvVar,
|
||||
)
|
||||
.map(Some);
|
||||
}
|
||||
|
||||
if let Some(conda_env) = conda_environment_from_env(system, CondaEnvironmentKind::Child) {
|
||||
return resolve_environment(system, &conda_env, SysPrefixPathOrigin::CondaPrefixVar)
|
||||
.map(Some);
|
||||
}
|
||||
|
||||
if let Some(project_root) = project_root {
|
||||
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,
|
||||
) {
|
||||
if let Some((path, origin)) = Self::virtual_environment_candidate(project_root, system) {
|
||||
let is_local_venv = matches!(origin, SysPrefixPathOrigin::LocalVenv);
|
||||
match resolve_environment(system, &path, origin) {
|
||||
Ok(environment) => return Ok(Some(environment)),
|
||||
Err(err) if !is_local_venv => return Err(err),
|
||||
Err(err) => {
|
||||
if system.is_directory(&virtual_env_directory) {
|
||||
if system.is_directory(&path) {
|
||||
tracing::debug!(
|
||||
"Ignoring automatically detected virtual environment at `{}`: {}",
|
||||
&virtual_env_directory,
|
||||
&path,
|
||||
err
|
||||
);
|
||||
}
|
||||
@@ -346,6 +327,24 @@ impl PythonEnvironment {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the virtual environment location to try and its origin, without checking whether
|
||||
/// it exists. `VIRTUAL_ENV` takes precedence over a child Conda environment, followed by the
|
||||
/// project's `.venv`. System Python fallbacks are handled by [`Self::discover`].
|
||||
pub fn virtual_environment_candidate(
|
||||
project_root: Option<&SystemPath>,
|
||||
system: &dyn System,
|
||||
) -> Option<(SystemPathBuf, SysPrefixPathOrigin)> {
|
||||
if let Ok(virtual_env) = system.env_var(EnvVars::VIRTUAL_ENV) {
|
||||
return Some((virtual_env.into(), SysPrefixPathOrigin::VirtualEnvVar));
|
||||
}
|
||||
|
||||
if let Some(conda_env) = conda_environment_from_env(system, CondaEnvironmentKind::Child) {
|
||||
return Some((conda_env, SysPrefixPathOrigin::CondaPrefixVar));
|
||||
}
|
||||
|
||||
project_root.map(|root| (root.join(".venv"), SysPrefixPathOrigin::LocalVenv))
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
path: impl AsRef<SystemPath>,
|
||||
origin: SysPrefixPathOrigin,
|
||||
|
||||
+19
-20
@@ -246,29 +246,25 @@ fn run_test(
|
||||
|
||||
let configuration = test.configuration();
|
||||
|
||||
let site_packages_paths = if configuration.dependencies().is_some() {
|
||||
let python = if configuration.dependencies().is_some() {
|
||||
// If dependencies were specified, use the venv we just set up
|
||||
let environment = PythonEnvironment::new(
|
||||
&venv_for_external_dependencies,
|
||||
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 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()
|
||||
Some(venv_for_external_dependencies.as_path())
|
||||
} else {
|
||||
vec![]
|
||||
configuration.python()
|
||||
};
|
||||
let python_environment = python.map(|python| {
|
||||
PythonEnvironment::new(python, SysPrefixPathOrigin::PythonCliFlag, db.system())
|
||||
.expect("Python environment to point to a valid path")
|
||||
});
|
||||
let site_packages_paths = python_environment
|
||||
.as_ref()
|
||||
.map(|environment| {
|
||||
environment
|
||||
.site_packages_paths(db.system())
|
||||
.expect("Python environment to be valid")
|
||||
.into_vec()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Make any relative extra-paths be relative to src_path
|
||||
let extra_paths = configuration
|
||||
@@ -286,6 +282,9 @@ fn run_test(
|
||||
.collect();
|
||||
|
||||
let settings = ProgramSettings {
|
||||
virtual_environment: python_environment
|
||||
.filter(PythonEnvironment::is_virtual)
|
||||
.map(|environment| environment.sys_prefix().to_path_buf()),
|
||||
python_version: PythonVersionWithSource {
|
||||
version: python_version,
|
||||
source: PythonVersionSource::Cli,
|
||||
|
||||
@@ -16,7 +16,6 @@ use ruff_db::system::{
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_python_parser::{Mode, ParseOptions, parse_unchecked};
|
||||
use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings};
|
||||
use ty_python_core::platform::PythonPlatform;
|
||||
use ty_python_core::program::{FallibleStrategy, ProgramSettings};
|
||||
use ty_python_core::{Db as _, ProgramFile, TestProgramDb};
|
||||
use ty_python_semantic::dependency::DependencyMetadata;
|
||||
@@ -65,15 +64,11 @@ impl TestDb {
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
let program_settings = ProgramSettings {
|
||||
python_version: PythonVersionWithSource::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings::new(vec![src_root])
|
||||
.to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
|
||||
.expect("Valid search path settings"),
|
||||
};
|
||||
program_settings.search_paths.try_register_static_roots(&db);
|
||||
db.program_settings = program_settings;
|
||||
let search_paths = SearchPathSettings::new(vec![src_root])
|
||||
.to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
|
||||
.expect("Valid search path settings");
|
||||
search_paths.try_register_static_roots(&db);
|
||||
db.program_settings.search_paths = search_paths;
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
@@ -574,14 +574,6 @@ kind = "inherent_method"
|
||||
level = "expect"
|
||||
reason = "TextSize intentionally provides both checked and saturating arithmetic"
|
||||
|
||||
[[override]]
|
||||
lint = "hawk::dead_public"
|
||||
crate = "ty_project"
|
||||
item = "db::changes::ChangeResult::custom_stdlib_changed"
|
||||
kind = "inherent_method"
|
||||
level = "expect"
|
||||
reason = "change results expose both project and custom-stdlib invalidation state"
|
||||
|
||||
[[override]]
|
||||
lint = "hawk::dead_public"
|
||||
crate = "ruff_python_ast"
|
||||
|
||||
Reference in new issue
Block a user