Skip to content

[ty] Infer the Python version from the environment if feasible #18057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/ruff_graph/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl ModuleDb {
Program::from_settings(
&db,
ProgramSettings {
python_version,
python_version: Some(python_version),
python_platform: PythonPlatform::default(),
search_paths,
},
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_ide/src/inlay_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::latest_ty(),
python_version: Some(PythonVersion::latest_ty()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::latest_ty(),
python_version: Some(PythonVersion::latest_ty()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_version: Some(PythonVersion::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
},
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl Options {
default
});
ProgramSettings {
python_version,
python_version: Some(python_version),
python_platform,
search_paths: self.to_search_path_settings(project_root, system),
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ pub(crate) mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: self.python_version,
python_version: Some(self.python_version),
python_platform: self.python_platform,
search_paths: SearchPathSettings::new(vec![src_root]),
},
Expand Down
96 changes: 55 additions & 41 deletions crates/ty_python_semantic/src/module_resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use ruff_python_ast::PythonVersion;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
use crate::site_packages::{PythonEnvironment, SitePackagesDiscoveryError, SysPrefixPathOrigin};
use crate::site_packages::{PythonEnvironment, SysPrefixPathOrigin};
use crate::{Program, PythonPath, SearchPathSettings};

use super::module::{Module, ModuleKind};
Expand Down Expand Up @@ -160,6 +160,11 @@ pub struct SearchPaths {
site_packages: Vec<SearchPath>,

typeshed_versions: TypeshedVersions,

/// The Python version for the search paths, if any.
///
/// This is read from the `pyvenv.cfg` if present.
python_version: Option<PythonVersion>,
}

impl SearchPaths {
Expand Down Expand Up @@ -234,7 +239,7 @@ impl SearchPaths {

static_paths.push(stdlib_path);

let site_packages_paths = match python_path {
let (site_packages_paths, python_version) = match python_path {
PythonPath::SysPrefix(sys_prefix, origin) => {
tracing::debug!(
"Discovering site-packages paths from sys-prefix `{sys_prefix}` ({origin}')"
Expand All @@ -243,8 +248,9 @@ impl SearchPaths {
// than the one resolved in the program settings because it indicates
// that the `target-version` is incorrectly configured or that the
// venv is out of date.
PythonEnvironment::new(sys_prefix, *origin, system)
.and_then(|env| env.site_packages_directories(system))?
PythonEnvironment::new(sys_prefix.clone(), *origin, system).and_then(|env| {
Ok((env.site_packages_directories(system)?, env.python_version()))
})?
}

PythonPath::Resolve(target, origin) => {
Expand All @@ -270,45 +276,47 @@ impl SearchPaths {
// handle the error.
.unwrap_or(target);

PythonEnvironment::new(root, *origin, system)
.and_then(|venv| venv.site_packages_directories(system))?
PythonEnvironment::new(root, *origin, system).and_then(|env| {
Ok((env.site_packages_directories(system)?, env.python_version()))
})?
}

PythonPath::Discover(root) => {
tracing::debug!("Discovering virtual environment in `{root}`");
let virtual_env_path = discover_venv_in(db.system(), root);
if let Some(virtual_env_path) = virtual_env_path {
tracing::debug!("Found `.venv` folder at `{}`", virtual_env_path);

let handle_invalid_virtual_env = |error: SitePackagesDiscoveryError| {
tracing::debug!(
"Ignoring automatically detected virtual environment at `{}`: {}",
virtual_env_path,
error
);
vec![]
};

match PythonEnvironment::new(
virtual_env_path.clone(),
SysPrefixPathOrigin::LocalVenv,
system,
) {
Ok(venv) => venv
.site_packages_directories(system)
.unwrap_or_else(handle_invalid_virtual_env),
Err(error) => handle_invalid_virtual_env(error),
}
} else {
tracing::debug!("No virtual environment found");
vec![]
}
discover_venv_in(db.system(), root)
.and_then(|virtual_env_path| {
tracing::debug!("Found `.venv` folder at `{}`", virtual_env_path);

PythonEnvironment::new(
virtual_env_path.clone(),
SysPrefixPathOrigin::LocalVenv,
system,
)
.and_then(|env| {
Ok((env.site_packages_directories(system)?, env.python_version()))
})
.inspect_err(|err| {
tracing::debug!(
"Ignoring automatically detected virtual environment at `{}`: {}",
virtual_env_path,
err
);
})
.ok()
})
.unwrap_or_else(|| {
tracing::debug!("No virtual environment found");
(vec![], None)
})
}

PythonPath::KnownSitePackages(paths) => paths
.iter()
.map(|path| canonicalize(path, system))
.collect(),
PythonPath::KnownSitePackages(paths) => (
paths
.iter()
.map(|path| canonicalize(path, system))
.collect(),
None,
),
};

let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len());
Expand Down Expand Up @@ -342,6 +350,7 @@ impl SearchPaths {
static_paths,
site_packages,
typeshed_versions,
python_version,
})
}

Expand All @@ -366,6 +375,10 @@ impl SearchPaths {
pub(super) fn typeshed_versions(&self) -> &TypeshedVersions {
&self.typeshed_versions
}

pub fn python_version(&self) -> Option<PythonVersion> {
self.python_version
}
}

/// Collect all dynamic search paths. For each `site-packages` path:
Expand All @@ -384,6 +397,7 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
static_paths,
site_packages,
typeshed_versions: _,
python_version: _,
} = Program::get(db).search_paths(db);

let mut dynamic_paths = Vec::new();
Expand Down Expand Up @@ -1489,7 +1503,7 @@ mod tests {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::PY38,
python_version: Some(PythonVersion::PY38),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
Expand Down Expand Up @@ -1995,7 +2009,7 @@ not_a_directory
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_version: Some(PythonVersion::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
Expand Down Expand Up @@ -2068,7 +2082,7 @@ not_a_directory
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_version: Some(PythonVersion::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
Expand Down Expand Up @@ -2108,7 +2122,7 @@ not_a_directory
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_version: Some(PythonVersion::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings {
extra_paths: vec![],
Expand Down
4 changes: 2 additions & 2 deletions crates/ty_python_semantic/src/module_resolver/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ impl TestCaseBuilder<MockedTypeshed> {
Program::from_settings(
&db,
ProgramSettings {
python_version,
python_version: Some(python_version),
python_platform,
search_paths: SearchPathSettings {
extra_paths: vec![],
Expand Down Expand Up @@ -293,7 +293,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
Program::from_settings(
&db,
ProgramSettings {
python_version,
python_version: Some(python_version),
python_platform,
search_paths: SearchPathSettings {
python_path: PythonPath::KnownSitePackages(vec![site_packages.clone()]),
Expand Down
18 changes: 12 additions & 6 deletions crates/ty_python_semantic/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ impl Program {
search_paths,
} = settings;

tracing::info!("Python version: Python {python_version}, platform: {python_platform}");

let search_paths = SearchPaths::from_settings(db, &search_paths)
.with_context(|| "Invalid search path settings")?;

let python_version = python_version
.or_else(|| search_paths.python_version())
.unwrap_or(PythonVersion::latest_ty());

tracing::info!("Python version: Python {python_version}, platform: {python_platform}");

Ok(
Program::builder(python_version, python_platform, search_paths)
.durability(Durability::HIGH)
Expand All @@ -56,9 +60,11 @@ impl Program {
self.set_python_platform(db).to(python_platform);
}

if python_version != self.python_version(db) {
tracing::debug!("Updating python version: Python {python_version}");
self.set_python_version(db).to(python_version);
if let Some(python_version) = python_version {
if python_version != self.python_version(db) {
tracing::debug!("Updating python version: Python {python_version}");
self.set_python_version(db).to(python_version);
}
}

self.update_search_paths(db, &search_paths)?;
Expand Down Expand Up @@ -89,7 +95,7 @@ impl Program {
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ProgramSettings {
pub python_version: PythonVersion,
pub python_version: Option<PythonVersion>,
pub python_platform: PythonPlatform,
pub search_paths: SearchPathSettings,
}
Expand Down
7 changes: 7 additions & 0 deletions crates/ty_python_semantic/src/site_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ impl PythonEnvironment {
Self::System(env) => env.site_packages_directories(system),
}
}

pub(crate) fn python_version(&self) -> Option<PythonVersion> {
match self {
Self::Virtual(env) => env.version,
Self::System(_) => None,
}
}
}

/// Abstraction for a Python virtual environment.
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_test/src/assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ mod tests {
let mut db = Db::setup();

let settings = ProgramSettings {
python_version: PythonVersion::default(),
python_version: Some(PythonVersion::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(Vec::new()),
};
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ fn run_test(
let configuration = test.configuration();

let settings = ProgramSettings {
python_version,
python_version: Some(python_version),
python_platform: configuration
.python_platform()
.unwrap_or(PythonPlatform::Identifier("linux".to_string())),
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_test/src/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ mod tests {
let mut db = crate::db::Db::setup();

let settings = ProgramSettings {
python_version: PythonVersion::default(),
python_version: Some(PythonVersion::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(Vec::new()),
};
Expand Down
2 changes: 1 addition & 1 deletion fuzz/fuzz_targets/ty_check_invalid_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn setup_db() -> TestDb {
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_version: Some(PythonVersion::default()),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![src_root]),
},
Expand Down
Loading